diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 38f6472b..0df7cef5 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -3,6 +3,7 @@ name: Maven Build and Deployment on: push: branches: [ master ] + tags: [ 'lmdbjava-*' ] pull_request: branches: [ master ] @@ -84,7 +85,14 @@ jobs: gpg-private-key: ${{ secrets.gpg_private_key }} gpg-passphrase: MAVEN_GPG_PASSPHRASE + - name: Get project version + id: version + run: echo "version=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_OUTPUT + - name: Publish Maven package + if: | + (github.ref_type == 'branch' && contains(steps.version.outputs.version, '-SNAPSHOT')) || + (github.ref_type == 'tag' && !contains(steps.version.outputs.version, '-SNAPSHOT')) run: mvn -B -Pcentral-deploy deploy -DskipTests env: MAVEN_GPG_PASSPHRASE: ${{ secrets.gpg_passphrase }} diff --git a/.gitignore b/.gitignore index f46b8b6f..5f3ff273 100644 --- a/.gitignore +++ b/.gitignore @@ -16,5 +16,4 @@ dependency-reduced-pom.xml gpg-sign.json mvn-sync.json secrets.tar -lmdb pom.xml.versionsBackup diff --git a/pom.xml b/pom.xml index 696ba9ff..5c1501c1 100644 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,7 @@ 1.22.0 - 3.27.6 + 3.27.7 3.2.1 - 0.9.1 0.9.0 2.29 1.28.0 @@ -39,7 +38,7 @@ 2.2.18 5.14.1 4.6 - 0.9.33-2 + 0.9.33-5 3.5.0 3.14.1 3.9.0 @@ -85,12 +84,6 @@ ${guava.version} test - - com.jakewharton.byteunits - byteunits - ${byteunits.version} - test - io.netty netty-buffer @@ -146,6 +139,7 @@ LICENSE.txt **/*.md + **/*.csv lmdb/** licenses/** @@ -224,19 +218,19 @@ - + [${maven.enforcer.mvn},) [${maven.enforcer.java},) - - + + true - + diff --git a/src/main/java/org/lmdbjava/AbstractFlagSet.java b/src/main/java/org/lmdbjava/AbstractFlagSet.java new file mode 100644 index 00000000..2e917515 --- /dev/null +++ b/src/main/java/org/lmdbjava/AbstractFlagSet.java @@ -0,0 +1,250 @@ +/* + * 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. + * + * @param The type of the flags in this set. Must extend {@link MaskedFlag} and {@link Enum}. + */ +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)); + } + + @Override + public int getMask() { + return mask; + } + + @Override + public Set getFlags() { + return flags; + } + + @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 String toString() { + return FlagSet.asString(this); + } + + 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); + } + } + + /** + * 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 final class Builder & MaskedFlag, S extends FlagSet> { + + final Class type; + final EnumSet enumSet; + final Function, S> constructor; + final Function singletonSetConstructor; + final Supplier emptySetSupplier; + + 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 setFlags(final Collection flags) { + clear(); + if (flags != null) { + for (E flag : flags) { + if (flag != null) { + enumSet.add(flag); + } + } + } + return this; + } + + /** + * Replaces any flags already set in the builder with the passed flags. + * + * @param flags The flags to set in the builder. + * @return this builder instance. + */ + @SafeVarargs + public final Builder setFlags(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; + } + + /** + * Adds a single flag in the builder. + * + * @param flag The flag to set in the builder. + * @return this builder instance. + */ + public Builder addFlag(final E flag) { + if (flag != null) { + enumSet.add(flag); + } + return this; + } + + /** + * Adds multiple flag in the builder. + * + * @param flags The flags to set in the builder. + * @return this builder instance. + */ + public Builder addFlags(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..a3c339bf 100644 --- a/src/main/java/org/lmdbjava/BufferProxy.java +++ b/src/main/java/org/lmdbjava/BufferProxy.java @@ -16,10 +16,6 @@ package org.lmdbjava; import static java.lang.Long.BYTES; -import static org.lmdbjava.DbiFlags.MDB_INTEGERKEY; -import static org.lmdbjava.DbiFlags.MDB_UNSIGNEDKEY; -import static org.lmdbjava.MaskedFlag.isSet; -import static org.lmdbjava.MaskedFlag.mask; import java.util.Comparator; import jnr.ffi.Pointer; @@ -75,30 +71,22 @@ protected BufferProxy() {} *

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

The provided comparator must strictly match the lexicographical order of keys in the native + * LMDB database. * * @return a comparator that can be used (never null) */ - protected abstract Comparator getUnsignedComparator(); + public Comparator getComparator() { + return getComparator(DbiFlagSet.empty()); + } /** * Called when the MDB_val should be set to reflect the passed buffer. This buffer @@ -138,4 +126,13 @@ protected Comparator getComparator(DbiFlags... flags) { final KeyVal keyVal() { return new KeyVal<>(this); } + + /** + * Create a new {@link Key} to hold pointers for this buffer proxy. + * + * @return a non-null key holder + */ + final Key key() { + return new Key<>(this); + } } diff --git a/src/main/java/org/lmdbjava/ByteArrayProxy.java b/src/main/java/org/lmdbjava/ByteArrayProxy.java index 853521e0..82b7721c 100644 --- a/src/main/java/org/lmdbjava/ByteArrayProxy.java +++ b/src/main/java/org/lmdbjava/ByteArrayProxy.java @@ -36,9 +36,6 @@ public final class ByteArrayProxy extends BufferProxy { private static final MemoryManager MEM_MGR = RUNTIME.getMemoryManager(); - private static final Comparator signedComparator = ByteArrayProxy::compareArraysSigned; - private static final Comparator unsignedComparator = ByteArrayProxy::compareArrays; - private ByteArrayProxy() {} /** @@ -48,7 +45,7 @@ private ByteArrayProxy() {} * @param o2 right operand (required) * @return as specified by {@link Comparable} interface */ - public static int compareArrays(final byte[] o1, final byte[] o2) { + public static int compareLexicographically(final byte[] o1, final byte[] o2) { requireNonNull(o1); requireNonNull(o2); if (o1 == o2) { @@ -68,26 +65,6 @@ public static int compareArrays(final byte[] o1, final byte[] o2) { return o1.length - o2.length; } - /** - * Compare two byte arrays. - * - * @param b1 left operand (required) - * @param b2 right operand (required) - * @return as specified by {@link Comparable} interface - */ - public static int compareArraysSigned(final byte[] b1, final byte[] b2) { - requireNonNull(b1); - requireNonNull(b2); - - if (b1 == b2) return 0; - - for (int i = 0; i < min(b1.length, b2.length); ++i) { - if (b1[i] != b2[i]) return b1[i] - b2[i]; - } - - return b1.length - b2.length; - } - @Override protected byte[] allocate() { return new byte[0]; @@ -104,13 +81,8 @@ protected byte[] getBytes(final byte[] buffer) { } @Override - protected Comparator getSignedComparator() { - return signedComparator; - } - - @Override - protected Comparator getUnsignedComparator() { - return unsignedComparator; + public Comparator getComparator(final DbiFlagSet dbiFlagSet) { + return ByteArrayProxy::compareLexicographically; } @Override diff --git a/src/main/java/org/lmdbjava/ByteBufProxy.java b/src/main/java/org/lmdbjava/ByteBufProxy.java index 2866e874..bcbb6ebf 100644 --- a/src/main/java/org/lmdbjava/ByteBufProxy.java +++ b/src/main/java/org/lmdbjava/ByteBufProxy.java @@ -23,6 +23,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.PooledByteBufAllocator; import java.lang.reflect.Field; +import java.nio.ByteOrder; import java.util.Comparator; import jnr.ffi.Pointer; @@ -44,13 +45,6 @@ public final class ByteBufProxy extends BufferProxy { private static final String FIELD_NAME_ADDRESS = "memoryAddress"; private static final String FIELD_NAME_LENGTH = "length"; private static final String NAME = "io.netty.buffer.PooledUnsafeDirectByteBuf"; - private static final Comparator comparator = - (o1, o2) -> { - requireNonNull(o1); - requireNonNull(o2); - - return o1.compareTo(o2); - }; private final long lengthOffset; private final long addressOffset; @@ -81,6 +75,71 @@ public ByteBufProxy(final PooledByteBufAllocator allocator) { } } + /** + * Lexicographically compare two buffers. + * + * @param o1 left operand (required) + * @param o2 right operand (required) + * @return as specified by {@link Comparable} interface + */ + public static int compareLexicographically(final ByteBuf o1, final ByteBuf o2) { + requireNonNull(o1); + requireNonNull(o2); + return o1.compareTo(o2); + } + + /** + * Buffer comparator specifically for 4/8 byte keys that are unsigned ints/longs, i.e. when using + * MDB_INTEGER_KEY/MDB_INTEGERDUP. Compares the buffers numerically. + * + * @param o1 left operand (required) + * @param o2 right operand (required) + * @return as specified by {@link Comparable} interface + */ + public static int compareAsIntegerKeys(final ByteBuf o1, final ByteBuf o2) { + requireNonNull(o1); + requireNonNull(o2); + // Both buffers should be same length according to LMDB API. + // From the LMDB docs for MDB_INTEGER_KEY + // numeric keys in native byte order: either unsigned int or size_t. The keys must all be of the + // same size. + final int len1 = o1.readableBytes(); + final int len2 = o2.readableBytes(); + if (len1 != len2) { + throw new RuntimeException( + "Length mismatch, len1: " + + len1 + + ", len2: " + + len2 + + ". Lengths must be identical and either 4 or 8 bytes."); + } + if (len1 == 8) { + final long lw; + final long rw; + if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) { + lw = o1.readLongLE(); + rw = o2.readLongLE(); + } else { + lw = o1.readLong(); + rw = o2.readLong(); + } + return Long.compareUnsigned(lw, rw); + } else if (len1 == 4) { + final int lw; + final int rw; + if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) { + lw = o1.readIntLE(); + rw = o2.readIntLE(); + } else { + lw = o1.readInt(); + rw = o2.readInt(); + } + return Integer.compareUnsigned(lw, rw); + } else { + return compareLexicographically(o1, o2); + } + } + static Field findField(final String c, final String name) { Class clazz; try { @@ -114,13 +173,12 @@ protected ByteBuf allocate() { } @Override - protected Comparator getSignedComparator() { - return comparator; - } - - @Override - protected Comparator getUnsignedComparator() { - return comparator; + public Comparator getComparator(final DbiFlagSet dbiFlagSet) { + if (dbiFlagSet.areAnySet(DbiFlagSet.INTEGER_KEY_FLAGS)) { + return ByteBufProxy::compareAsIntegerKeys; + } else { + return ByteBufProxy::compareLexicographically; + } } @Override diff --git a/src/main/java/org/lmdbjava/ByteBufferProxy.java b/src/main/java/org/lmdbjava/ByteBufferProxy.java index cf75562c..b5dfca0b 100644 --- a/src/main/java/org/lmdbjava/ByteBufferProxy.java +++ b/src/main/java/org/lmdbjava/ByteBufferProxy.java @@ -27,6 +27,7 @@ import java.lang.reflect.Field; import java.nio.Buffer; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.ArrayDeque; import java.util.Comparator; import jnr.ffi.Pointer; @@ -57,6 +58,8 @@ public final class ByteBufferProxy { /** The safe, reflective {@link ByteBuffer} proxy for this system. Guaranteed to never be null. */ public static final BufferProxy PROXY_SAFE; + private static final ByteOrder NATIVE_ORDER = ByteOrder.nativeOrder(); + static { PROXY_SAFE = new ReflectiveProxy(); PROXY_OPTIMAL = getProxyOptimal(); @@ -92,16 +95,6 @@ abstract static class AbstractByteBufferProxy extends BufferProxy { protected static final String FIELD_NAME_ADDRESS = "address"; protected static final String FIELD_NAME_CAPACITY = "capacity"; - private static final Comparator signedComparator = - (o1, o2) -> { - requireNonNull(o1); - requireNonNull(o2); - - return o1.compareTo(o2); - }; - private static final Comparator unsignedComparator = - AbstractByteBufferProxy::compareBuff; - /** * A thread-safe pool for a given length. If the buffer found is valid (ie not of a negative * length) then that buffer is used. If no valid buffer is found, a new buffer is created. @@ -116,7 +109,7 @@ abstract static class AbstractByteBufferProxy extends BufferProxy { * @param o2 right operand (required) * @return as specified by {@link Comparable} interface */ - public static int compareBuff(final ByteBuffer o1, final ByteBuffer o2) { + public static int compareLexicographically(final ByteBuffer o1, final ByteBuffer o2) { requireNonNull(o1); requireNonNull(o2); @@ -146,6 +139,55 @@ public static int compareBuff(final ByteBuffer o1, final ByteBuffer o2) { return o1.remaining() - o2.remaining(); } + /** + * Buffer comparator specifically for 4/8 byte keys that are unsigned ints/longs, i.e. when + * using MDB_INTEGER_KEY/MDB_INTEGERDUP. Compares the buffers numerically. + * + * @param o1 left operand (required) + * @param o2 right operand (required) + * @return as specified by {@link Comparable} interface + */ + public static int compareAsIntegerKeys(final ByteBuffer o1, final ByteBuffer o2) { + requireNonNull(o1); + requireNonNull(o2); + // Both buffers should be same length according to LMDB API. + // From the LMDB docs for MDB_INTEGER_KEY + // numeric keys in native byte order: either unsigned int or size_t. The keys must all be of + // the same size. + final int len1 = o1.limit(); + final int len2 = o2.limit(); + if (len1 != len2) { + throw new RuntimeException( + "Length mismatch, len1: " + + len1 + + ", len2: " + + len2 + + ". Lengths must be identical and either 4 or 8 bytes."); + } + // Keys for MDB_INTEGER_KEY are written in native order so ensure we read them in that order + o1.order(NATIVE_ORDER); + o2.order(NATIVE_ORDER); + // TODO it might be worth the DbiBuilder having a method to capture fixedKeyLength() or -1 + // for variable length keys. This can be passed to getComparator(..) so it can return a + // comparator that doesn't need to test the length every time. There may be other benefits + // to the Dbi knowing the key length if it is fixed. + if (len1 == 8) { + final long lw = o1.getLong(0); + final long rw = o2.getLong(0); + return Long.compareUnsigned(lw, rw); + } else if (len1 == 4) { + final int lw = o1.getInt(0); + final int rw = o2.getInt(0); + return Integer.compareUnsigned(lw, rw); + } else { + // size_t and int are likely to be 8bytes and 4bytes respectively on 64bit. + // If 32bit then would be 4/2 respectively. + // Short.compareUnsigned is not available in Java8. + // For now just fall back to our standard comparator + return compareLexicographically(o1, o2); + } + } + static Field findField(final Class c, final String name) { Class clazz = c; do { @@ -180,13 +222,12 @@ protected final ByteBuffer allocate() { } @Override - protected Comparator getSignedComparator() { - return signedComparator; - } - - @Override - protected Comparator getUnsignedComparator() { - return unsignedComparator; + public Comparator getComparator(final DbiFlagSet dbiFlagSet) { + if (dbiFlagSet.areAnySet(DbiFlagSet.INTEGER_KEY_FLAGS)) { + return AbstractByteBufferProxy::compareAsIntegerKeys; + } else { + return AbstractByteBufferProxy::compareLexicographically; + } } @Override diff --git a/src/main/java/org/lmdbjava/ByteUnit.java b/src/main/java/org/lmdbjava/ByteUnit.java new file mode 100644 index 00000000..1c37ad24 --- /dev/null +++ b/src/main/java/org/lmdbjava/ByteUnit.java @@ -0,0 +1,71 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +/** Simple {@link Enum} for converting various IEC and SI byte units down to a number of bytes. */ +public enum ByteUnit { + + /** IEC/SI byte unit for bytes. */ + BYTES(1L), + + /** IEC byte unit for 1024 bytes. */ + KIBIBYTES(1_024L), + /** IEC byte unit for 1024^2 bytes. */ + MEBIBYTES(1_048_576L), + /** IEC byte unit for 1024^3 bytes. */ + GIBIBYTES(1_073_741_824L), + /** IEC byte unit for 1024^4 bytes. */ + TEBIBYTES(1_099_511_627_776L), + /** IEC byte unit for 1024^5 bytes. */ + PEBIBYTES(1_125_899_906_842_624L), + + /** SI byte unit for 1000 bytes. */ + KILOBYTES(1_000L), + /** SI byte unit for 1000^2 bytes. */ + MEGABYTES(1_000_000L), + /** SI byte unit for 1000^3 bytes. */ + GIGABYTES(1_000_000_000L), + /** SI byte unit for 1000^4 bytes. */ + TERABYTES(1_000_000_000_000L), + /** SI byte unit for 1000^5 bytes. */ + PETABYTES(1_000_000_000_000_000L), + ; + + private final long factor; + + ByteUnit(long factor) { + this.factor = factor; + } + + /** + * Convert the value in this byte unit into bytes. + * + * @param value The value to convert. + * @return The number of bytes. + */ + public long toBytes(final long value) { + return value * factor; + } + + /** + * Gets factor to apply when converting this unit into bytes. + * + * @return The factor to apply when converting this unit into bytes. + */ + public long getFactor() { + return factor; + } +} diff --git a/src/main/java/org/lmdbjava/CopyFlagSet.java b/src/main/java/org/lmdbjava/CopyFlagSet.java new file mode 100644 index 00000000..c8e35477 --- /dev/null +++ b/src/main/java/org/lmdbjava/CopyFlagSet.java @@ -0,0 +1,77 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import java.nio.file.Path; +import java.util.Collection; +import java.util.Objects; + +/** An immutable set of flags for use when performing a {@link Env#copy(Path, CopyFlagSet)}. */ +public interface CopyFlagSet extends FlagSet { + + /** An immutable empty {@link CopyFlagSet}. */ + CopyFlagSet EMPTY = CopyFlagSetImpl.EMPTY; + + /** + * Gets the immutable empty {@link CopyFlagSet} instance. + * + * @return The immutable empty {@link CopyFlagSet} instance. + */ + static CopyFlagSet empty() { + return CopyFlagSetImpl.EMPTY; + } + + /** + * Creates an immutable {@link CopyFlagSet} containing copyFlag. + * + * @param copyFlag The flag to include in the {@link CopyFlagSet} + * @return An immutable {@link CopyFlagSet} containing just copyFlag. + */ + static CopyFlagSet of(final CopyFlags copyFlag) { + Objects.requireNonNull(copyFlag); + return copyFlag; + } + + /** + * Creates an immutable {@link CopyFlagSet} containing copyFlags. + * + * @param copyFlags The flags to include in the {@link CopyFlagSet}. + * @return An immutable {@link CopyFlagSet} containing copyFlags. + */ + static CopyFlagSet of(final CopyFlags... copyFlags) { + return builder().setFlags(copyFlags).build(); + } + + /** + * Creates an immutable {@link CopyFlagSet} containing copyFlags. + * + * @param copyFlags The flags to include in the {@link CopyFlagSet}. + * @return An immutable {@link CopyFlagSet} containing copyFlags. + */ + static CopyFlagSet of(final Collection copyFlags) { + return builder().setFlags(copyFlags).build(); + } + + /** + * Create a builder for building an {@link CopyFlagSet}. + * + * @return A builder instance for building an {@link CopyFlagSet}. + */ + static AbstractFlagSet.Builder builder() { + return new AbstractFlagSet.Builder<>( + CopyFlags.class, CopyFlagSetImpl::new, copyFlag -> copyFlag, () -> CopyFlagSetImpl.EMPTY); + } +} diff --git a/src/main/java/org/lmdbjava/CopyFlagSetEmpty.java b/src/main/java/org/lmdbjava/CopyFlagSetEmpty.java new file mode 100644 index 00000000..f18af382 --- /dev/null +++ b/src/main/java/org/lmdbjava/CopyFlagSetEmpty.java @@ -0,0 +1,19 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +class CopyFlagSetEmpty extends AbstractFlagSet.AbstractEmptyFlagSet + implements CopyFlagSet {} diff --git a/src/main/java/org/lmdbjava/CopyFlagSetImpl.java b/src/main/java/org/lmdbjava/CopyFlagSetImpl.java new file mode 100644 index 00000000..a566fc2a --- /dev/null +++ b/src/main/java/org/lmdbjava/CopyFlagSetImpl.java @@ -0,0 +1,27 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import java.util.EnumSet; + +class CopyFlagSetImpl extends AbstractFlagSet implements CopyFlagSet { + + static final CopyFlagSet EMPTY = new CopyFlagSetEmpty(); + + CopyFlagSetImpl(final EnumSet flags) { + super(flags); + } +} diff --git a/src/main/java/org/lmdbjava/CopyFlags.java b/src/main/java/org/lmdbjava/CopyFlags.java index 4365563c..e3677baf 100644 --- a/src/main/java/org/lmdbjava/CopyFlags.java +++ b/src/main/java/org/lmdbjava/CopyFlags.java @@ -15,8 +15,12 @@ */ package org.lmdbjava; -/** Flags for use when performing a {@link Env#copy(java.io.File, org.lmdbjava.CopyFlags...)}. */ -public enum CopyFlags implements MaskedFlag { +import java.nio.file.Path; +import java.util.EnumSet; +import java.util.Set; + +/** Flags for use when performing a {@link Env#copy(Path, CopyFlagSet)}. */ +public enum CopyFlags implements MaskedFlag, CopyFlagSet { /** Compacting copy: Omit free space from copy, and renumber all pages sequentially. */ MDB_CP_COMPACT(0x01); @@ -31,4 +35,29 @@ public enum CopyFlags implements MaskedFlag { public int getMask() { return mask; } + + @Override + public Set getFlags() { + return EnumSet.of(this); + } + + @Override + public boolean isSet(final CopyFlags flag) { + return this == flag; + } + + @Override + public int size() { + return 1; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public String toString() { + return FlagSet.asString(this); + } } diff --git a/src/main/java/org/lmdbjava/Cursor.java b/src/main/java/org/lmdbjava/Cursor.java index f7fcbc41..0e320930 100644 --- a/src/main/java/org/lmdbjava/Cursor.java +++ b/src/main/java/org/lmdbjava/Cursor.java @@ -20,8 +20,6 @@ import static org.lmdbjava.Dbi.KeyNotFoundException.MDB_NOTFOUND; import static org.lmdbjava.Env.SHOULD_CHECK; import static org.lmdbjava.Library.LIB; -import static org.lmdbjava.MaskedFlag.isSet; -import static org.lmdbjava.MaskedFlag.mask; import static org.lmdbjava.PutFlags.MDB_MULTIPLE; import static org.lmdbjava.PutFlags.MDB_NODUPDATA; import static org.lmdbjava.PutFlags.MDB_NOOVERWRITE; @@ -98,22 +96,41 @@ public long count() { return longByReference.longValue(); } + /** + * @deprecated Instead use {@link Cursor#delete(PutFlagSet)}.


Delete current key/data pair. + *

This function deletes the key/data pair to which the cursor refers. + * @param flags flags (either null or {@link PutFlags#MDB_NODUPDATA} + */ + @Deprecated + public void delete(final PutFlags... flags) { + delete(PutFlagSet.of(flags)); + } + + /** + * Delete current key/data pair. + * + *

This function deletes the key/data pair to which the cursor refers. + */ + public void delete() { + delete(PutFlagSet.EMPTY); + } + /** * Delete current key/data pair. * *

This function deletes the key/data pair to which the cursor refers. * - * @param f flags (either null or {@link PutFlags#MDB_NODUPDATA} + * @param flags flags (either null or {@link PutFlags#MDB_NODUPDATA} */ - public void delete(final PutFlags... f) { + public void delete(final PutFlagSet flags) { if (SHOULD_CHECK) { env.checkNotClosed(); checkNotClosed(); txn.checkReady(); txn.checkWritesAllowed(); } - final int flags = mask(true, f); - checkRc(LIB.mdb_cursor_del(ptrCursor, flags)); + final PutFlagSet putFlagSet = flags != null ? flags : PutFlagSet.EMPTY; + checkRc(LIB.mdb_cursor_del(ptrCursor, putFlagSet.getMask())); } /** @@ -203,6 +220,10 @@ public T key() { return kv.key(); } + KeyVal keyVal() { + return kv; + } + /** * Position at last key/data item. * @@ -230,6 +251,20 @@ public boolean prev() { return seek(MDB_PREV); } + /** + * @deprecated Use {@link Cursor#put(Object, Object, PutFlagSet)} instead.


Store by cursor. + *

This function stores key/data pairs into the database. + * @param key key to store + * @param val data to store + * @param flags options for this operation + * @return true if the value was put, false if MDB_NOOVERWRITE or MDB_NODUPDATA were set and the + * key/value existed already. + */ + @Deprecated + public boolean put(final T key, final T val, final PutFlags... flags) { + return put(key, val, PutFlagSet.of(flags)); + } + /** * Store by cursor. * @@ -237,14 +272,29 @@ public boolean prev() { * * @param key key to store * @param val data to store - * @param op options for this operation * @return true if the value was put, false if MDB_NOOVERWRITE or MDB_NODUPDATA were set and the * key/value existed already. */ - public boolean put(final T key, final T val, final PutFlags... op) { + public boolean put(final T key, final T val) { + return put(key, val, PutFlagSet.EMPTY); + } + + /** + * Store by cursor. + * + *

This function stores key/data pairs into the database. + * + * @param key key to store + * @param val data to store + * @param flags options for this operation + * @return true if the value was put, false if MDB_NOOVERWRITE or MDB_NODUPDATA were set and the + * key/value existed already. + */ + public boolean put(final T key, final T val, final PutFlagSet flags) { if (SHOULD_CHECK) { requireNonNull(key); requireNonNull(val); + requireNonNull(flags); env.checkNotClosed(); checkNotClosed(); txn.checkReady(); @@ -252,12 +302,11 @@ public boolean put(final T key, final T val, final PutFlags... op) { } final Pointer transientKey = kv.keyIn(key); final Pointer transientVal = kv.valIn(val); - final int mask = mask(true, op); - final int rc = LIB.mdb_cursor_put(ptrCursor, kv.pointerKey(), kv.pointerVal(), mask); + final int rc = LIB.mdb_cursor_put(ptrCursor, kv.pointerKey(), kv.pointerVal(), flags.getMask()); if (rc == MDB_KEYEXIST) { - if (isSet(mask, MDB_NOOVERWRITE)) { + if (flags.isSet(MDB_NOOVERWRITE)) { kv.valOut(); // marked as in,out in LMDB C docs - } else if (!isSet(mask, MDB_NODUPDATA)) { + } else if (!flags.isSet(MDB_NODUPDATA)) { checkRc(rc); } return false; @@ -270,6 +319,39 @@ public boolean put(final T key, final T val, final PutFlags... op) { return true; } + /** + * @deprecated Use {@link Cursor#put(Object, Object, PutFlagSet)} instead.


Put multiple + * values into the database in one MDB_MULTIPLE operation. + *

The database must have been opened with {@link DbiFlags#MDB_DUPFIXED}. The buffer must + * contain fixed-sized values to be inserted. The size of each element is calculated from the + * buffer's size divided by the given element count. For example, to populate 10 X 4 byte + * integers at once, present a buffer of 40 bytes and specify the element as 10. + * @param key key to store in the database (not null) + * @param val value to store in the database (not null) + * @param elements number of elements contained in the passed value buffer + * @param flags options for operation (must set MDB_MULTIPLE) + */ + @Deprecated + public void putMultiple(final T key, final T val, final int elements, final PutFlags... flags) { + putMultiple(key, val, elements, PutFlagSet.of(flags)); + } + + /** + * Put multiple values into the database in one MDB_MULTIPLE operation. + * + *

The database must have been opened with {@link DbiFlags#MDB_DUPFIXED}. The buffer must + * contain fixed-sized values to be inserted. The size of each element is calculated from the + * buffer's size divided by the given element count. For example, to populate 10 X 4 byte integers + * at once, present a buffer of 40 bytes and specify the element as 10. + * + * @param key key to store in the database (not null) + * @param val value to store in the database (not null) + * @param elements number of elements contained in the passed value buffer + */ + public void putMultiple(final T key, final T val, final int elements) { + putMultiple(key, val, elements, PutFlagSet.EMPTY); + } + /** * Put multiple values into the database in one MDB_MULTIPLE operation. * @@ -281,9 +363,10 @@ public boolean put(final T key, final T val, final PutFlags... op) { * @param key key to store in the database (not null) * @param val value to store in the database (not null) * @param elements number of elements contained in the passed value buffer - * @param op options for operation (must set MDB_MULTIPLE) + * @param flags options for operation (must set MDB_MULTIPLE) Either a {@link + * PutFlagSet} or a single {@link PutFlags}. */ - public void putMultiple(final T key, final T val, final int elements, final PutFlags... op) { + public void putMultiple(final T key, final T val, final int elements, final PutFlagSet flags) { if (SHOULD_CHECK) { requireNonNull(txn); requireNonNull(key); @@ -291,14 +374,14 @@ public void putMultiple(final T key, final T val, final int elements, final PutF env.checkNotClosed(); txn.checkReady(); txn.checkWritesAllowed(); + if (!flags.isSet(MDB_MULTIPLE)) { + throw new IllegalArgumentException("Must set " + MDB_MULTIPLE + " flag"); + } } - final int mask = mask(true, op); - if (SHOULD_CHECK && !isSet(mask, MDB_MULTIPLE)) { - throw new IllegalArgumentException("Must set " + MDB_MULTIPLE + " flag"); - } + final Pointer transientKey = txn.kv().keyIn(key); final Pointer dataPtr = txn.kv().valInMulti(val, elements); - final int rc = LIB.mdb_cursor_put(ptrCursor, txn.kv().pointerKey(), dataPtr, mask); + final int rc = LIB.mdb_cursor_put(ptrCursor, txn.kv().pointerKey(), dataPtr, flags.getMask()); checkRc(rc); ReferenceUtil.reachabilityFence0(transientKey); ReferenceUtil.reachabilityFence0(dataPtr); @@ -329,23 +412,59 @@ public void renew(final Txn newTxn) { this.txn = newTxn; } + /** + * @deprecated Use {@link Cursor#reserve(Object, int, PutFlagSet)} instead.


Reserve space for + * data of the given size, but don't copy the given val. Instead, return a pointer to the + * reserved space, which the caller can fill in later - before the next update operation or + * the transaction ends. This saves an extra memcpy if the data is being generated later. LMDB + * does nothing else with this memory, the caller is expected to modify all of the space + * requested. + *

This flag must not be specified if the database was opened with MDB_DUPSORT + * @param key key to store in the database (not null) + * @param size size of the value to be stored in the database (not null) + * @param flags options for this operation + * @return a buffer that can be used to modify the value + */ + @Deprecated + public T reserve(final T key, final int size, final PutFlags... flags) { + return reserve(key, size, PutFlagSet.of(flags)); + } + + /** + * Reserve space for data of the given size, but don't copy the given val. Instead, return a + * pointer to the reserved space, which the caller can fill in later - before the next update + * operation or the transaction ends. This saves an extra {@code memcpy} if the data is being + * generated later. LMDB does nothing else with this memory, the caller is expected to modify all + * the space requested. + * + *

This flag must not be specified if the database was opened with MDB_DUPSORT + * + * @param key key to store in the database (not null) + * @param size size of the value to be stored in the database (not null) + * @return a buffer that can be used to modify the value + */ + public T reserve(final T key, final int size) { + return reserve(key, size, PutFlagSet.EMPTY); + } + /** * Reserve space for data of the given size, but don't copy the given val. Instead, return a * pointer to the reserved space, which the caller can fill in later - before the next update * operation or the transaction ends. This saves an extra memcpy if the data is being generated - * later. LMDB does nothing else with this memory, the caller is expected to modify all of the - * space requested. + * later. LMDB does nothing else with this memory, the caller is expected to modify all the space + * requested. * *

This flag must not be specified if the database was opened with MDB_DUPSORT * * @param key key to store in the database (not null) * @param size size of the value to be stored in the database (not null) - * @param op options for this operation + * @param flags options for this operation * @return a buffer that can be used to modify the value */ - public T reserve(final T key, final int size, final PutFlags... op) { + public T reserve(final T key, final int size, final PutFlagSet flags) { if (SHOULD_CHECK) { requireNonNull(key); + requireNonNull(flags); env.checkNotClosed(); checkNotClosed(); txn.checkReady(); @@ -353,8 +472,9 @@ public T reserve(final T key, final int size, final PutFlags... op) { } final Pointer transientKey = kv.keyIn(key); final Pointer transientVal = kv.valIn(size); - final int flags = mask(true, op) | MDB_RESERVE.getMask(); - checkRc(LIB.mdb_cursor_put(ptrCursor, kv.pointerKey(), kv.pointerVal(), flags)); + // This is inconsistent with putMultiple which require MDB_MULTIPLE to be in the set. + final int flagsMask = flags.getMaskWith(MDB_RESERVE); + checkRc(LIB.mdb_cursor_put(ptrCursor, kv.pointerKey(), kv.pointerVal(), flagsMask)); kv.valOut(); ReferenceUtil.reachabilityFence0(transientKey); ReferenceUtil.reachabilityFence0(transientVal); diff --git a/src/main/java/org/lmdbjava/CursorIterable.java b/src/main/java/org/lmdbjava/CursorIterable.java index 6a03bd90..65fc1023 100644 --- a/src/main/java/org/lmdbjava/CursorIterable.java +++ b/src/main/java/org/lmdbjava/CursorIterable.java @@ -21,10 +21,14 @@ import static org.lmdbjava.CursorIterable.State.REQUIRES_NEXT_OP; import static org.lmdbjava.CursorIterable.State.TERMINATED; import static org.lmdbjava.GetOp.MDB_SET_RANGE; +import static org.lmdbjava.Library.LIB; import java.util.Comparator; import java.util.Iterator; import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.function.Supplier; +import jnr.ffi.Pointer; import org.lmdbjava.KeyRangeType.CursorOp; import org.lmdbjava.KeyRangeType.IteratorOp; @@ -38,7 +42,7 @@ */ public final class CursorIterable implements Iterable>, AutoCloseable { - private final Comparator comparator; + private final RangeComparator rangeComparator; private final Cursor cursor; private final KeyVal entry; private boolean iteratorReturned; @@ -46,16 +50,32 @@ public final class CursorIterable implements Iterable txn, final Dbi dbi, final KeyRange range, final Comparator comparator) { + final Txn txn, + final Dbi dbi, + final KeyRange range, + final Comparator comparator, + final BufferProxy proxy) { this.cursor = dbi.openCursor(txn); this.range = range; - this.comparator = comparator; this.entry = new KeyVal<>(); + + if (comparator != null) { + // User supplied Java-side comparator so use that + this.rangeComparator = new JavaRangeComparator<>(range, comparator, cursor::key); + } else { + // No Java-side comparator, so call down to LMDB to do the comparison + this.rangeComparator = new LmdbRangeComparator<>(txn, dbi, cursor, range, proxy); + } } @Override public void close() { cursor.close(); + try { + rangeComparator.close(); + } catch (Exception e) { + throw new RuntimeException(e); + } } /** @@ -95,13 +115,13 @@ public KeyVal next() { @Override public void remove() { - cursor.delete(); + cursor.delete(PutFlags.EMPTY); } }; } private void executeCursorOp(final CursorOp op) { - final boolean found; + boolean found; switch (op) { case FIRST: found = cursor.first(); @@ -119,7 +139,31 @@ private void executeCursorOp(final CursorOp op) { found = cursor.get(range.getStart(), MDB_SET_RANGE); break; case GET_START_KEY_BACKWARD: - found = cursor.get(range.getStart(), MDB_SET_RANGE) || cursor.last(); + found = cursor.get(range.getStart(), MDB_SET_RANGE); + if (found) { + if (!range.getType().isDirectionForward() + && range.getType().isStartKeyRequired() + && range.getType().isStartKeyInclusive()) { + // We need to ensure we move to the last matching key if using DUPSORT, see issue 267 + boolean loop = true; + while (loop) { + if (rangeComparator.compareToStartKey() <= 0) { + found = cursor.next(); + if (!found) { + // We got to the end so move last. + found = cursor.last(); + loop = false; + } + } else { + // We have moved past so go back one. + found = cursor.prev(); + loop = false; + } + } + } + } else { + found = cursor.last(); + } break; default: throw new IllegalStateException("Unknown cursor operation"); @@ -129,8 +173,7 @@ private void executeCursorOp(final CursorOp op) { } private void executeIteratorOp() { - final IteratorOp op = - range.getType().iteratorOp(range.getStart(), range.getStop(), entry.key(), comparator); + final IteratorOp op = range.getType().iteratorOp(entry.key(), rangeComparator); switch (op) { case CALL_NEXT_OP: executeCursorOp(range.getType().nextOp()); @@ -219,4 +262,100 @@ enum State { RELEASED, TERMINATED } + + static class JavaRangeComparator implements RangeComparator { + + private final Comparator comparator; + private final Supplier currentKeySupplier; + private final T start; + private final T stop; + + JavaRangeComparator( + final KeyRange range, + final Comparator comparator, + final Supplier currentKeySupplier) { + this.comparator = comparator; + this.currentKeySupplier = currentKeySupplier; + this.start = range.getStart(); + this.stop = range.getStop(); + } + + @Override + public int compareToStartKey() { + return comparator.compare(currentKeySupplier.get(), start); + } + + @Override + public int compareToStopKey() { + return comparator.compare(currentKeySupplier.get(), stop); + } + + @Override + public void close() throws Exception { + // Nothing to close + } + } + + /** + * Calls down to mdb_cmp to make use of the comparator that LMDB uses for insertion order. Has a + * very slight overhead as compared to {@link JavaRangeComparator}. + */ + private static class LmdbRangeComparator implements RangeComparator { + + private final Pointer txnPointer; + private final Pointer dbiPointer; + private final Pointer cursorKeyPointer; + private final Key startKey; + private final Key stopKey; + private final Pointer startKeyPointer; + private final Pointer stopKeyPointer; + + public LmdbRangeComparator( + final Txn txn, + final Dbi dbi, + final Cursor cursor, + final KeyRange range, + final BufferProxy proxy) { + txnPointer = Objects.requireNonNull(txn).pointer(); + dbiPointer = Objects.requireNonNull(dbi).pointer(); + cursorKeyPointer = Objects.requireNonNull(cursor).keyVal().pointerKey(); + // Allocate buffers for use with the start/stop keys if required. + // Saves us copying bytes on each comparison + Objects.requireNonNull(range); + startKey = createKey(range.getStart(), proxy); + stopKey = createKey(range.getStop(), proxy); + startKeyPointer = startKey != null ? startKey.pointer() : null; + stopKeyPointer = stopKey != null ? stopKey.pointer() : null; + } + + @Override + public int compareToStartKey() { + return LIB.mdb_cmp(txnPointer, dbiPointer, cursorKeyPointer, startKeyPointer); + } + + @Override + public int compareToStopKey() { + return LIB.mdb_cmp(txnPointer, dbiPointer, cursorKeyPointer, stopKeyPointer); + } + + @Override + public void close() { + if (startKey != null) { + startKey.close(); + } + if (stopKey != null) { + stopKey.close(); + } + } + + private Key createKey(final T keyBuffer, final BufferProxy proxy) { + if (keyBuffer != null) { + final Key key = proxy.key(); + key.keyIn(keyBuffer); + return key; + } else { + return null; + } + } + } } diff --git a/src/main/java/org/lmdbjava/Dbi.java b/src/main/java/org/lmdbjava/Dbi.java index 5449c172..d2afdf8f 100644 --- a/src/main/java/org/lmdbjava/Dbi.java +++ b/src/main/java/org/lmdbjava/Dbi.java @@ -31,6 +31,7 @@ import static org.lmdbjava.PutFlags.MDB_RESERVE; import static org.lmdbjava.ResultCodeMapper.checkRc; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -48,12 +49,26 @@ */ public final class Dbi { - private final ComparatorCallback ccb; + @SuppressWarnings("FieldCanBeLocal") // Needs to be instance variable for FFI + private final ComparatorCallback callbackComparator; + private boolean cleaned; + // Used for CursorIterable KeyRange testing and/or native callbacks private final Comparator comparator; private final Env env; private final byte[] name; private final Pointer ptr; + private final BufferProxy proxy; + private final DbiFlagSet dbiFlagSet; + + Dbi( + final Env env, + final Txn txn, + final byte[] name, + final BufferProxy proxy, + final DbiFlagSet dbiFlagSet) { + this(env, txn, name, null, false, proxy, dbiFlagSet); + } Dbi( final Env env, @@ -62,38 +77,50 @@ public final class Dbi { final Comparator comparator, final boolean nativeCb, final BufferProxy proxy, - final DbiFlags... flags) { + final DbiFlagSet dbiFlagSet) { + if (SHOULD_CHECK) { + if (nativeCb && comparator == null) { + throw new IllegalArgumentException("Is nativeCb is true, you must supply a comparator"); + } + requireNonNull(env); requireNonNull(txn); + requireNonNull(proxy); + requireNonNull(dbiFlagSet); txn.checkReady(); } this.env = env; this.name = name == null ? null : Arrays.copyOf(name, name.length); - if (comparator == null) { - this.comparator = proxy.getComparator(flags); - } else { - this.comparator = comparator; - } - final int flagsMask = mask(true, flags); + this.proxy = proxy; + this.comparator = comparator; + this.dbiFlagSet = dbiFlagSet; final Pointer dbiPtr = allocateDirect(RUNTIME, ADDRESS); - checkRc(LIB.mdb_dbi_open(txn.pointer(), name, flagsMask, dbiPtr)); + checkRc(LIB.mdb_dbi_open(txn.pointer(), name, this.dbiFlagSet.getMask(), dbiPtr)); ptr = dbiPtr.getPointer(0); if (nativeCb) { - this.ccb = - (keyA, keyB) -> { - final T compKeyA = proxy.out(proxy.allocate(), keyA); - final T compKeyB = proxy.out(proxy.allocate(), keyB); - final int result = this.comparator.compare(compKeyA, compKeyB); - proxy.deallocate(compKeyA); - proxy.deallocate(compKeyB); - return result; - }; - LIB.mdb_set_compare(txn.pointer(), ptr, ccb); + // LMDB will call back to this comparator for insertion/iteration order + this.callbackComparator = createCallbackComparator(proxy); + LIB.mdb_set_compare(txn.pointer(), ptr, callbackComparator); } else { - ccb = null; + callbackComparator = null; } } + private ComparatorCallback createCallbackComparator(final BufferProxy proxy) { + return (keyA, keyB) -> { + final T compKeyA = proxy.out(proxy.allocate(), keyA); + final T compKeyB = proxy.out(proxy.allocate(), keyB); + final int result = this.comparator.compare(compKeyA, compKeyB); + proxy.deallocate(compKeyA); + proxy.deallocate(compKeyB); + return result; + }; + } + + Pointer pointer() { + return ptr; + } + /** * Close the database handle (normally unnecessary; use with caution). * @@ -248,12 +275,57 @@ public T get(final Txn txn, final T key) { /** * Obtains the name of this database. * - * @return the name (may be null) + * @return The name (it maybe null) */ public byte[] getName() { return name == null ? null : Arrays.copyOf(name, name.length); } + /** + * Convert the passed name into bytes using the default {@link Charset}. + * + * @param name The name to convert. + * @return The name as a byte[] or null if name is null. + */ + public static byte[] getNameBytes(final String name) { + return name == null ? null : name.getBytes(Env.DEFAULT_NAME_CHARSET); + } + + /** + * Obtains the name of this database, using the {@link Env#DEFAULT_NAME_CHARSET} {@link Charset}. + * + * @return The name of this database, using the {@link Env#DEFAULT_NAME_CHARSET} {@link Charset}. + */ + public String getNameAsString() { + return getNameAsString(Env.DEFAULT_NAME_CHARSET); + } + + /** + * Obtains the name of this database, using the supplied {@link Charset}. + * + * @param charset The {@link Charset} to use when converting the DB from a byte[] to a {@link + * String}. + * @return The name of the database. If this is the unnamed database an empty string will be + * returned. + * @throws RuntimeException if the name can't be decoded. + */ + public String getNameAsString(final Charset charset) { + return getNameAsString(this.name, charset); + } + + static String getNameAsString(final byte[] name, final Charset charset) { + if (name == null) { + return ""; + } else { + // Assume a UTF8 encoding as we don't know, thus swallow if it fails + try { + return new String(name, requireNonNull(charset)); + } catch (Exception e) { + throw new RuntimeException("Unable to decode database name using charset " + charset); + } + } + } + /** * Iterate the database from the first item and forwards. * @@ -278,7 +350,7 @@ public CursorIterable iterate(final Txn txn, final KeyRange range) { env.checkNotClosed(); txn.checkReady(); } - return new CursorIterable<>(txn, this, range, comparator); + return new CursorIterable<>(txn, this, range, comparator, proxy); } /** @@ -288,6 +360,7 @@ public CursorIterable iterate(final Txn txn, final KeyRange range) { * @return the list of flags this Dbi was created with */ public List listFlags(final Txn txn) { + // TODO we could just return what is in dbiFlagSet, rather than hitting LMDB. if (SHOULD_CHECK) { env.checkNotClosed(); } @@ -337,15 +410,48 @@ public Cursor openCursor(final Txn txn) { * * @param key key to store in the database (not null) * @param val value to store in the database (not null) - * @see #put(org.lmdbjava.Txn, java.lang.Object, java.lang.Object, org.lmdbjava.PutFlags...) + * @see #put(Txn, Object, Object, PutFlagSet) */ public void put(final T key, final T val) { try (Txn txn = env.txnWrite()) { - put(txn, key, val); + put(txn, key, val, PutFlagSet.EMPTY); txn.commit(); } } + /** + * @param txn transaction handle (not null; not committed; must be R-W) + * @param key key to store in the database (not null) + * @param val value to store in the database (not null) + * @param flags Special options for this operation + * @return true if the value was put, false if MDB_NOOVERWRITE or MDB_NODUPDATA were set and the + * key/value existed already. + * @deprecated Use {@link Dbi#put(Txn, Object, Object, PutFlagSet)} instead, with a statically + * held {@link PutFlagSet}.


+ *

Store a key/value pair in the database. + *

This function stores key/data pairs in the database. The default behavior is to enter + * the new key/data pair, replacing any previously existing key if duplicates are disallowed, + * or adding a duplicate data item if duplicates are allowed ({@link DbiFlags#MDB_DUPSORT}). + */ + @Deprecated + public boolean put(final Txn txn, final T key, final T val, final PutFlags... flags) { + return put(txn, key, val, PutFlagSet.of(flags)); + } + + /** + * Store a key/value pair in the database. + * + * @param txn transaction handle (not null; not committed; must be R-W) + * @param key key to store in the database (not null) + * @param val value to store in the database (not null) + * @return true if the value was put, false if MDB_NOOVERWRITE or MDB_NODUPDATA were set and the + * key/value existed already. + * @see #put(Txn, Object, Object, PutFlagSet) + */ + public boolean put(final Txn txn, final T key, final T val) { + return put(txn, key, val, PutFlagSet.EMPTY); + } + /** * Store a key/value pair in the database. * @@ -356,28 +462,29 @@ public void put(final T key, final T val) { * @param txn transaction handle (not null; not committed; must be R-W) * @param key key to store in the database (not null) * @param val value to store in the database (not null) - * @param flags Special options for this operation + * @param flags Special options for this operation. * @return true if the value was put, false if MDB_NOOVERWRITE or MDB_NODUPDATA were set and the * key/value existed already. */ - public boolean put(final Txn txn, final T key, final T val, final PutFlags... flags) { + public boolean put(final Txn txn, final T key, final T val, final PutFlagSet flags) { if (SHOULD_CHECK) { requireNonNull(txn); requireNonNull(key); requireNonNull(val); + requireNonNull(flags); env.checkNotClosed(); txn.checkReady(); txn.checkWritesAllowed(); } final Pointer transientKey = txn.kv().keyIn(key); final Pointer transientVal = txn.kv().valIn(val); - final int mask = mask(true, flags); final int rc = - LIB.mdb_put(txn.pointer(), ptr, txn.kv().pointerKey(), txn.kv().pointerVal(), mask); + LIB.mdb_put( + txn.pointer(), ptr, txn.kv().pointerKey(), txn.kv().pointerVal(), flags.getMask()); if (rc == MDB_KEYEXIST) { - if (isSet(mask, MDB_NOOVERWRITE)) { + if (flags.isSet(MDB_NOOVERWRITE)) { txn.kv().valOut(); // marked as in,out in LMDB C docs - } else if (!isSet(mask, MDB_NODUPDATA)) { + } else if (!flags.isSet(MDB_NODUPDATA)) { checkRc(rc); } return false; @@ -415,7 +522,7 @@ public T reserve(final Txn txn, final T key, final int size, final PutFlags.. } final Pointer transientKey = txn.kv().keyIn(key); final Pointer transientVal = txn.kv().valIn(size); - final int flags = mask(true, op) | MDB_RESERVE.getMask(); + final int flags = mask(op) | MDB_RESERVE.getMask(); checkRc(LIB.mdb_put(txn.pointer(), ptr, txn.kv().pointerKey(), txn.kv().pointerVal(), flags)); txn.kv().valOut(); // marked as in,out in LMDB C docs ReferenceUtil.reachabilityFence0(transientKey); @@ -454,6 +561,17 @@ private void clean() { cleaned = true; } + @Override + public String toString() { + String name; + try { + name = getNameAsString(); + } catch (Exception e) { + name = "?"; + } + return "Dbi{" + "name='" + name + "', dbiFlagSet=" + dbiFlagSet + '}'; + } + /** The specified DBI was changed unexpectedly. */ public static final class BadDbiException extends LmdbNativeException { diff --git a/src/main/java/org/lmdbjava/DbiBuilder.java b/src/main/java/org/lmdbjava/DbiBuilder.java new file mode 100644 index 00000000..29bbb8f5 --- /dev/null +++ b/src/main/java/org/lmdbjava/DbiBuilder.java @@ -0,0 +1,424 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Objects; + +/** + * Staged builder for building a {@link Dbi} + * + * @param buffer type + */ +public final class DbiBuilder { + + private final Env env; + private final BufferProxy proxy; + private final boolean readOnly; + private byte[] name; + + DbiBuilder(final Env env, final BufferProxy proxy, final boolean readOnly) { + this.env = Objects.requireNonNull(env); + this.proxy = Objects.requireNonNull(proxy); + this.readOnly = readOnly; + } + + /** + * Create the {@link Dbi} with the passed name. + * + *

The name will be converted into bytes using {@link StandardCharsets#UTF_8}. + * + * @param name The name of the database or null for the unnamed database (see also {@link + * DbiBuilder#withoutDbName()}) + * @return The next builder stage. + */ + public Stage2 setDbName(final String name) { + // Null name is allowed so no null check + final byte[] nameBytes = name == null ? null : name.getBytes(Env.DEFAULT_NAME_CHARSET); + return setDbName(nameBytes); + } + + /** + * Create the {@link Dbi} with the passed name in byte[] form. + * + * @param name The name of the database in byte form. + * @return The next builder stage. + */ + public Stage2 setDbName(final byte[] name) { + // Null name is allowed so no null check + this.name = name; + return new Stage2<>(this); + } + + /** + * Create the {@link Dbi} without a name. + * + *

Equivalent to passing null to {@link DbiBuilder#setDbName(String)} or {@link + * DbiBuilder#setDbName(byte[])}. + * + *

Note: The 'unnamed database' is used by LMDB to store the names of named databases, with the + * database name being the key. Use of the unnamed database is intended for simple applications + * with only one database. + * + * @return The next builder stage. + */ + public Stage2 withoutDbName() { + return setDbName((byte[]) null); + } + + /** + * Intermediate builder stage for constructing a {@link Dbi}. + * + * @param buffer type + */ + public static final class Stage2 { + + private final DbiBuilder dbiBuilder; + + private ComparatorFactory comparatorFactory; + private ComparatorType comparatorType; + + private Stage2(final DbiBuilder dbiBuilder) { + this.dbiBuilder = dbiBuilder; + } + + /** + * This is the default choice when it comes to choosing a comparator. If you + * are not sure of the implications of the other methods then use this one as it is likely what + * you want and also probably the most performant. + * + *

With this option, {@link CursorIterable} will make use of the LmdbJava's default Java-side + * comparators when comparing iteration keys to the start/stop keys. LMDB will use its own + * comparator for controlling insertion order in the database. The two comparators are + * functionally identical. + * + *

This option may be slightly more performant than when using {@link + * Stage2#withNativeComparator()} which calls down to LMDB for ALL comparison operations. + * + *

If you do not intend to use {@link CursorIterable} then it doesn't matter whether you + * choose {@link Stage2#withNativeComparator()}, {@link Stage2#withDefaultComparator()} or + * {@link Stage2#withIteratorComparator(ComparatorFactory)} as these comparators will never be + * used. + * + * @return The next builder stage. + */ + public Stage3 withDefaultComparator() { + this.comparatorType = ComparatorType.DEFAULT; + return new Stage3<>(this); + } + + /** + * With this option, {@link CursorIterable} will call down to LMDB's {@code mdb_cmp} method when + * comparing iteration keys to start/stop keys. This ensures LmdbJava is comparing start/stop + * keys using the same comparator that is used for insertion order into the db. + * + *

This option may be slightly less performant than when using {@link + * Stage2#withDefaultComparator()} as it needs to call down to LMDB to perform the comparisons, + * however it guarantees that {@link CursorIterable} key comparison matches LMDB key comparison. + * + *

If you do not intend to use {@link CursorIterable} then it doesn't matter whether you + * choose {@link Stage2#withNativeComparator()}, {@link Stage2#withDefaultComparator()} or + * {@link Stage2#withIteratorComparator(ComparatorFactory)} as these comparators will never be + * used. + * + * @return The next builder stage. + */ + public Stage3 withNativeComparator() { + this.comparatorType = ComparatorType.NATIVE; + return new Stage3<>(this); + } + + /** + * Provide a java-side {@link Comparator} that LMDB will call back to for all + * comparison operations. Therefore, it will be called by LMDB to manage database + * insertion/iteration order. It will also be used for {@link CursorIterable} start/stop key + * comparisons. + * + *

It can be useful if you need to sort your database using some other method, e.g. signed + * keys or case-insensitive order. Note, if you need keys stored in reverse order, see {@link + * DbiFlags#MDB_REVERSEKEY} and {@link DbiFlags#MDB_REVERSEDUP}. + * + *

As this requires LMDB to call back to java, this will be less performant than using LMDB's + * default comparators, but allows for total control over the order in which entries are stored + * in the database. + * + * @param comparatorFactory A factory to create a comparator. {@link + * ComparatorFactory#create(DbiFlagSet)} will be called once during the initialisation of + * the {@link Dbi}. It must not return null. + * @return The next builder stage. + */ + public Stage3 withCallbackComparator(final ComparatorFactory comparatorFactory) { + this.comparatorFactory = Objects.requireNonNull(comparatorFactory); + this.comparatorType = ComparatorType.CALLBACK; + return new Stage3<>(this); + } + + /** + * WARNING: Only use this if you fully understand the risks and implications. + *


+ * + *

With this option, {@link CursorIterable} will make use of the passed comparator for + * comparing iteration keys to start/stop keys. It has NO bearing on the + * insert/iteration order of the database (which is controlled by LMDB's own comparators). + * + *

It is vital that this comparator is functionally identical to the one + * used internally in LMDB for insertion/iteration order, else you will see unexpected behaviour + * when using {@link CursorIterable}. + * + *

If you do not intend to use {@link CursorIterable} then it doesn't matter whether you + * choose {@link Stage2#withNativeComparator()}, {@link Stage2#withDefaultComparator()} or + * {@link Stage2#withIteratorComparator(ComparatorFactory)} as these comparators will never be + * used. + * + * @param comparatorFactory The comparator to use with {@link CursorIterable}. {@link + * ComparatorFactory#create(DbiFlagSet)} will be called once during the initialisation of + * the {@link Dbi}. It must not return null. + * @return The next builder stage. + */ + public Stage3 withIteratorComparator(final ComparatorFactory comparatorFactory) { + this.comparatorFactory = Objects.requireNonNull(comparatorFactory); + this.comparatorType = ComparatorType.ITERATOR; + return new Stage3<>(this); + } + } + + /** + * Final stage builder for constructing a {@link Dbi}. + * + * @param buffer type + */ + public static final class Stage3 { + + private final Stage2 stage2; + private final AbstractFlagSet.Builder flagSetBuilder = + DbiFlagSet.builder(); + private Txn txn = null; + + private Stage3(Stage2 stage2) { + this.stage2 = stage2; + } + + /** + * Apply all the dbi flags supplied in dbiFlags. + * + *

Clears all flags currently set by previous calls to {@link + * Stage3#setDbiFlags(Collection)}, {@link Stage3#setDbiFlags(DbiFlags...)} or {@link + * Stage3#addDbiFlag(DbiFlags)}. + * + * @param dbiFlags to open the database with. A null {@link Collection} will just clear all set + * flags. Null items are ignored. + * @return This builder instance. + */ + public Stage3 setDbiFlags(final Collection dbiFlags) { + flagSetBuilder.clear(); + if (dbiFlags != null) { + dbiFlags.stream().filter(Objects::nonNull).forEach(this.flagSetBuilder::addFlag); + } + return this; + } + + /** + * Apply all the dbi flags supplied in dbiFlags. + * + *

Clears all flags currently set by previous calls to {@link + * Stage3#setDbiFlags(Collection)}, {@link Stage3#setDbiFlags(DbiFlags...)} or {@link + * Stage3#addDbiFlag(DbiFlags)}. + * + * @param dbiFlags to open the database with. A null array will just clear all set flags. Null + * items are ignored. + * @return This builder instance. + */ + public Stage3 setDbiFlags(final DbiFlags... dbiFlags) { + flagSetBuilder.clear(); + if (dbiFlags != null) { + Arrays.stream(dbiFlags).filter(Objects::nonNull).forEach(this.flagSetBuilder::addFlag); + } + return this; + } + + /** + * Apply all the dbi flags supplied in dbiFlags. + * + *

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

The caller MUST commit the transaction after calling {@link Stage3#open()}, in order to + * retain the Dbi in the Env. The caller is also responsible for + * closing the transaction. + * + *

If you don't call this method to supply a {@link Txn}, a {@link Txn} will be opened for + * the purpose of creating and opening the {@link Dbi}, then closed. Therefore, if you already + * have a transaction open, you should supply that to avoid one blocking the other. + * + * @param txn transaction to use (required; not closed). If the {@link Env} was opened with the + * {@link EnvFlags#MDB_RDONLY_ENV} flag, the {@link Txn} can be read-only, else it needs to + * be a read/write {@link Txn}. + * @return this builder instance. + */ + public Stage3 setTxn(final Txn txn) { + this.txn = Objects.requireNonNull(txn); + return this; + } + + /** + * Construct and open the {@link Dbi}. + * + *

If a {@link Txn} was supplied to the builder, it is the callers responsibility to commit + * and close the txn upon return from this method, else the created DB won't be retained. + * + * @return A newly constructed and opened {@link Dbi}. + */ + public Dbi open() { + final DbiBuilder dbiBuilder = stage2.dbiBuilder; + if (txn != null) { + return openDbi(txn, dbiBuilder); + } else { + try (final Txn localTxn = getTxn(dbiBuilder)) { + final Dbi dbi = openDbi(localTxn, dbiBuilder); + // even RO Txns require a commit to retain Dbi in Env + localTxn.commit(); + return dbi; + } + } + } + + private Txn getTxn(final DbiBuilder dbiBuilder) { + return dbiBuilder.readOnly ? dbiBuilder.env.txnRead() : dbiBuilder.env.txnWrite(); + } + + private Comparator getComparator( + final DbiBuilder dbiBuilder, + final ComparatorType comparatorType, + final DbiFlagSet dbiFlagSet) { + Comparator comparator = null; + switch (comparatorType) { + case DEFAULT: + // Get the appropriate default CursorIterable comparator based on the DbiFlags, + // e.g. MDB_INTEGERKEY may benefit from an optimised comparator. + comparator = dbiBuilder.proxy.getComparator(dbiFlagSet); + break; + case CALLBACK: + case ITERATOR: + comparator = stage2.comparatorFactory.create(dbiFlagSet); + Objects.requireNonNull(comparator, "comparatorFactory returned null"); + break; + case NATIVE: + break; + default: + throw new IllegalStateException("Unexpected comparatorType " + comparatorType); + } + return comparator; + } + + private Dbi openDbi(final Txn txn, final DbiBuilder dbiBuilder) { + final DbiFlagSet dbiFlagSet = flagSetBuilder.build(); + final ComparatorType comparatorType = stage2.comparatorType; + final Comparator comparator = getComparator(dbiBuilder, comparatorType, dbiFlagSet); + final boolean useNativeCallback = comparatorType == ComparatorType.CALLBACK; + return new Dbi<>( + dbiBuilder.env, + txn, + dbiBuilder.name, + comparator, + useNativeCallback, + dbiBuilder.proxy, + dbiFlagSet); + } + } + + private enum ComparatorType { + /** + * Default Java comparator for {@link CursorIterable} KeyRange testing, LMDB comparator for + * insertion/iteration order. + */ + DEFAULT, + /** Use LMDB native comparator for everything. */ + NATIVE, + /** Use the supplied custom Java-side comparator for everything. */ + CALLBACK, + /** + * Use the supplied custom Java-side comparator for {@link CursorIterable} KeyRange testing, + * LMDB comparator for insertion/iteration order. + */ + ITERATOR, + ; + } + + /** + * A factory for creating a {@link Comparator} from a {@link DbiFlagSet} + * + * @param The type of buffer that will be compared by the created {@link Comparator}. + */ + @FunctionalInterface + public interface ComparatorFactory { + + /** + * Creates a comparator for the supplied {@link DbiFlagSet}. This will only be called once + * during the initialisation of the {@link Dbi}. + * + * @param dbiFlagSet The flags set on the DB that the returned {@link Comparator} will be used + * by. The flags in the set may impact how the returned {@link Comparator} should behave. + * @return A {@link Comparator} applicable to the passed DB flags. + */ + Comparator create(final DbiFlagSet dbiFlagSet); + } +} diff --git a/src/main/java/org/lmdbjava/DbiFlagSet.java b/src/main/java/org/lmdbjava/DbiFlagSet.java new file mode 100644 index 00000000..82043fe3 --- /dev/null +++ b/src/main/java/org/lmdbjava/DbiFlagSet.java @@ -0,0 +1,79 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import java.util.Collection; +import java.util.Objects; + +/** An immutable set of flags for use when opening a {@link Dbi}. */ +public interface DbiFlagSet extends FlagSet { + + /** An immutable empty {@link DbiFlagSet}. */ + DbiFlagSet EMPTY = DbiFlagSetImpl.EMPTY; + + /** The set of {@link DbiFlags} that indicate unsigned integer keys are being used. */ + DbiFlagSet INTEGER_KEY_FLAGS = DbiFlagSet.of(DbiFlags.MDB_INTEGERKEY, DbiFlags.MDB_INTEGERDUP); + + /** + * Gets the immutable empty {@link DbiFlagSet} instance. + * + * @return The immutable empty {@link DbiFlagSet} instance. + */ + static DbiFlagSet empty() { + return DbiFlagSetImpl.EMPTY; + } + + /** + * Creates an immutable {@link DbiFlagSet} containing dbiFlag. + * + * @param dbiFlag The flag to include in the {@link DbiFlagSet} + * @return An immutable {@link DbiFlagSet} containing just dbiFlag. + */ + static DbiFlagSet of(final DbiFlags dbiFlag) { + Objects.requireNonNull(dbiFlag); + return dbiFlag; + } + + /** + * Creates an immutable {@link DbiFlagSet} containing dbiFlags. + * + * @param dbiFlags The flags to include in the {@link DbiFlagSet}. + * @return An immutable {@link DbiFlagSet} containing dbiFlags. + */ + static DbiFlagSet of(final DbiFlags... dbiFlags) { + return builder().setFlags(dbiFlags).build(); + } + + /** + * Creates an immutable {@link DbiFlagSet} containing dbiFlags. + * + * @param dbiFlags The flags to include in the {@link DbiFlagSet}. + * @return An immutable {@link DbiFlagSet} containing dbiFlags. + */ + static DbiFlagSet of(final Collection dbiFlags) { + return builder().setFlags(dbiFlags).build(); + } + + /** + * Create a builder for building an {@link DbiFlagSet}. + * + * @return A builder instance for building an {@link DbiFlagSet}. + */ + static AbstractFlagSet.Builder builder() { + return new AbstractFlagSet.Builder<>( + DbiFlags.class, DbiFlagSetImpl::new, dbiFlag -> dbiFlag, () -> DbiFlagSetImpl.EMPTY); + } +} diff --git a/src/main/java/org/lmdbjava/DbiFlagSetEmpty.java b/src/main/java/org/lmdbjava/DbiFlagSetEmpty.java new file mode 100644 index 00000000..cff91caf --- /dev/null +++ b/src/main/java/org/lmdbjava/DbiFlagSetEmpty.java @@ -0,0 +1,19 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +class DbiFlagSetEmpty extends AbstractFlagSet.AbstractEmptyFlagSet + implements DbiFlagSet {} diff --git a/src/main/java/org/lmdbjava/DbiFlagSetImpl.java b/src/main/java/org/lmdbjava/DbiFlagSetImpl.java new file mode 100644 index 00000000..a43e887f --- /dev/null +++ b/src/main/java/org/lmdbjava/DbiFlagSetImpl.java @@ -0,0 +1,27 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import java.util.EnumSet; + +class DbiFlagSetImpl extends AbstractFlagSet implements DbiFlagSet { + + static final DbiFlagSet EMPTY = new DbiFlagSetEmpty(); + + DbiFlagSetImpl(final EnumSet flags) { + super(flags); + } +} diff --git a/src/main/java/org/lmdbjava/DbiFlags.java b/src/main/java/org/lmdbjava/DbiFlags.java index 123ec9fd..9426db0f 100644 --- a/src/main/java/org/lmdbjava/DbiFlags.java +++ b/src/main/java/org/lmdbjava/DbiFlags.java @@ -15,8 +15,11 @@ */ package org.lmdbjava; +import java.util.EnumSet; +import java.util.Set; + /** Flags for use when opening a {@link Dbi}. */ -public enum DbiFlags implements MaskedFlag { +public enum DbiFlags implements MaskedFlag, DbiFlagSet { /** * Use reverse string keys. @@ -29,13 +32,24 @@ public enum DbiFlags implements MaskedFlag { * Use sorted duplicates. * *

Duplicate keys may be used in the database. Or, from another perspective, keys may have - * multiple data items, stored in sorted order. By default keys must be unique and may have only a - * single data item. + * multiple data items, stored in sorted order. By default, keys must be unique and may have only + * a single data item. */ MDB_DUPSORT(0x04), /** - * Numeric keys in native byte order: either unsigned int or size_t. The keys must all be of the - * same size. + * Numeric keys in native byte order: either unsigned int or size_t. The keys must all be + * of the same size. + * + *

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

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

For more information, see Numeric Keys in the + * LmdbJava wiki. */ MDB_INTEGERKEY(0x08), /** @@ -55,14 +69,6 @@ public enum DbiFlags implements MaskedFlag { * #MDB_INTEGERKEY} keys. */ MDB_INTEGERDUP(0x20), - /** - * Compare the numeric keys in native byte order and as unsigned. - * - *

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

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

Both buffer must have 4 or 8 bytes remaining + * + * @param o1 left operand (required) + * @param o2 right operand (required) + * @return as specified by {@link Comparable} interface + */ + public static int compareAsIntegerKeys(final DirectBuffer o1, final DirectBuffer o2) { + requireNonNull(o1); + requireNonNull(o2); + // Both buffers should be same len + final int len1 = o1.capacity(); + final int len2 = o2.capacity(); + if (len1 != len2) { + throw new RuntimeException( + "Length mismatch, len1: " + + len1 + + ", len2: " + + len2 + + ". Lengths must be identical and either 4 or 8 bytes."); + } + if (len1 == 8) { + final long lw = o1.getLong(0, NATIVE_ORDER); + final long rw = o2.getLong(0, NATIVE_ORDER); + return Long.compareUnsigned(lw, rw); + } else if (len1 == 4) { + final int lw = o1.getInt(0, NATIVE_ORDER); + final int rw = o2.getInt(0, NATIVE_ORDER); + return Integer.compareUnsigned(lw, rw); + } else { + // size_t and int are likely to be 8bytes and 4bytes respectively on 64bit. + // If 32bit then would be 4/2 respectively. + // Short.compareUnsigned is not available in Java8. + // For now just fall back to our standard comparator + return compareLexicographically(o1, o2); + } + } + @Override protected DirectBuffer allocate() { final ArrayDeque q = BUFFERS.get(); @@ -109,13 +145,12 @@ protected DirectBuffer allocate() { } @Override - protected Comparator getSignedComparator() { - return signedComparator; - } - - @Override - protected Comparator getUnsignedComparator() { - return unsignedComparator; + public Comparator getComparator(final DbiFlagSet dbiFlagSet) { + if (dbiFlagSet.areAnySet(DbiFlagSet.INTEGER_KEY_FLAGS)) { + return DirectBufferProxy::compareAsIntegerKeys; + } else { + return DirectBufferProxy::compareLexicographically; + } } @Override diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index 3db16119..4bc6cca8 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -16,24 +16,28 @@ package org.lmdbjava; import static java.lang.Boolean.getBoolean; -import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Objects.requireNonNull; import static org.lmdbjava.ByteBufferProxy.PROXY_OPTIMAL; import static org.lmdbjava.EnvFlags.MDB_NOSUBDIR; import static org.lmdbjava.EnvFlags.MDB_RDONLY_ENV; import static org.lmdbjava.Library.LIB; import static org.lmdbjava.Library.RUNTIME; -import static org.lmdbjava.MaskedFlag.isSet; -import static org.lmdbjava.MaskedFlag.mask; import static org.lmdbjava.ResultCodeMapper.checkRc; -import static org.lmdbjava.TxnFlags.MDB_RDONLY_TXN; import java.io.File; import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; import jnr.ffi.Pointer; import jnr.ffi.byref.IntByReference; import jnr.ffi.byref.PointerByReference; @@ -50,6 +54,12 @@ public final class Env implements AutoCloseable { /** Java system property name that can be set to disable optional checks. */ public static final String DISABLE_CHECKS_PROP = "lmdbjava.disable.checks"; + /** + * The default {@link Charset} used to convert DB names from a byte[] to a String or to encode a + * String as a byte[]. Only used if not explicit {@link Charset} is provided. + */ + public static final Charset DEFAULT_NAME_CHARSET = StandardCharsets.UTF_8; + /** * Indicates whether optional checks should be applied in LmdbJava. Optional checks are only * disabled in critical paths (see package-level JavaDocs). Non-critical paths have optional @@ -63,18 +73,24 @@ public final class Env implements AutoCloseable { private final BufferProxy proxy; private final Pointer ptr; private final boolean readOnly; + private final Path path; + private final EnvFlagSet envFlagSet; private Env( final BufferProxy proxy, final Pointer ptr, final boolean readOnly, - final boolean noSubDir) { + final boolean noSubDir, + final Path path, + final EnvFlagSet envFlagSet) { this.proxy = proxy; this.readOnly = readOnly; this.noSubDir = noSubDir; this.ptr = ptr; // cache max key size to avoid further JNI calls this.maxKeySize = LIB.mdb_env_get_maxkeysize(ptr); + this.path = path; + this.envFlagSet = envFlagSet; } /** @@ -98,16 +114,17 @@ public static Builder create(final BufferProxy proxy) { } /** - * Opens an environment with a single default database in 0664 mode using the {@link - * ByteBufferProxy#PROXY_OPTIMAL}. - * * @param path file system destination * @param size size in megabytes * @param flags the flags for this new environment * @return env the environment (never null) + * @deprecated Instead use {@link Env#create()} or {@link Env#create(BufferProxy)} + *

Opens an environment with a single default database in 0664 mode using the {@link + * ByteBufferProxy#PROXY_OPTIMAL}. */ + @Deprecated public static Env open(final File path, final int size, final EnvFlags... flags) { - return new Builder<>(PROXY_OPTIMAL).setMapSize(size * 1_024L * 1_024L).open(path, flags); + return new Builder<>(PROXY_OPTIMAL).setMapSize(size, ByteUnit.MEBIBYTES).open(path, flags); } /** @@ -141,12 +158,58 @@ public void close() { * * @param path writable destination path as described above * @param flags special options for this copy + * @deprecated Use {@link Env#copy(Path, CopyFlagSet)} */ + @Deprecated public void copy(final File path, final CopyFlags... flags) { requireNonNull(path); + copy(path.toPath(), CopyFlagSet.of(flags)); + } + + /** + * Copies an LMDB environment to the specified destination path. + * + *

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

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

Note: This call can trigger significant file size growth if run in parallel with write + * transactions, because it employs a read-only transaction. See long-lived transactions under + * "Caveats" in the LMDB native documentation. + * + * @param path writable destination path as described above + */ + public void copy(final Path path) { + copy(path, CopyFlagSet.EMPTY); + } + + /** + * Copies an LMDB environment to the specified destination path. + * + *

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

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

Note: This call can trigger significant file size growth if run in parallel with write + * transactions, because it employs a read-only transaction. See long-lived transactions under + * "Caveats" in the LMDB native documentation. + * + * @param path writable destination path as described above + * @param flags special options for this copy + */ + public void copy(final Path path, final CopyFlagSet flags) { + requireNonNull(path); + requireNonNull(flags); validatePath(path); - final int flagsMask = mask(true, flags); - checkRc(LIB.mdb_env_copy2(ptr, path.getAbsolutePath(), flagsMask)); + checkRc(LIB.mdb_env_copy2(ptr, path.toAbsolutePath().toString(), flags.getMask())); } /** @@ -162,19 +225,38 @@ public void copy(final File path, final CopyFlags... flags) { */ public List getDbiNames() { final List result = new ArrayList<>(); - final Dbi names = openDbi((byte[]) null); - try (Txn txn = txnRead(); - Cursor cursor = names.openCursor(txn)) { - if (!cursor.first()) { - return Collections.emptyList(); + // The unnamed DB is special so the names of the named DBs are held as keys in it. + try (final Txn readTxn = txnRead()) { + final Dbi unnamedDb = new Dbi<>(this, readTxn, null, proxy, DbiFlagSet.EMPTY); + try (final Cursor cursor = unnamedDb.openCursor(readTxn)) { + if (!cursor.first()) { + return Collections.emptyList(); + } + do { + final byte[] name = proxy.getBytes(cursor.key()); + result.add(name); + } while (cursor.next()); } - do { - final byte[] name = proxy.getBytes(cursor.key()); - result.add(name); - } while (cursor.next()); } + return Collections.unmodifiableList(result); + } - return result; + /** + * Obtain the DBI names. + * + *

This method is only compatible with {@link Env}s that use named databases. If an unnamed + * {@link Dbi} is being used to store data, this method will attempt to return all such keys from + * the unnamed database. + * + *

This method must not be called from concurrent threads. + * + * @return a list of DBI names (never null) + */ + public List getDbiNames(final Charset charset) { + final List dbiNames = getDbiNames(); + return dbiNames.stream() + .map(nameBytes -> Dbi.getNameAsString(nameBytes, charset)) + .collect(Collectors.toList()); } /** @@ -183,9 +265,23 @@ public List getDbiNames() { * @param mapSize the new size, in bytes */ public void setMapSize(final long mapSize) { + if (mapSize < 0) { + throw new IllegalArgumentException("Negative value; overflow?"); + } checkRc(LIB.mdb_env_set_mapsize(ptr, mapSize)); } + /** + * Set the size of the data memory map. + * + * @param mapSize new map size in the units of byteUnit. + * @param byteUnit The unit that mapSize is in. + */ + public void setMapSize(final long mapSize, final ByteUnit byteUnit) { + requireNonNull(byteUnit); + setMapSize(byteUnit.toBytes(mapSize)); + } + /** * Get the maximum size of keys and MDB_DUPSORT data we can write. * @@ -242,137 +338,159 @@ public boolean isReadOnly() { } /** - * Convenience method that opens a {@link Dbi} with a UTF-8 database name and default {@link - * Comparator} that is not invoked from native code. + * Returns a builder for creating and opening a {@link Dbi} instance in this {@link Env}. + * + *

The flag {@link DbiFlags#MDB_CREATE} needs to be set on the builder if you need to create a + * new database before opening it. * + * @return A new builder instance for creating/opening a {@link Dbi}. + */ + public DbiBuilder createDbi() { + return new DbiBuilder<>(this, proxy, readOnly); + } + + /** * @param name name of the database (or null if no name is required) * @param flags to open the database with * @return a database that is ready to use + * @deprecated Instead use {@link Env#createDbi()} + *

Convenience method that opens a {@link Dbi} with a UTF-8 database name and default + * {@link Comparator} that is not invoked from native code. */ + @Deprecated() public Dbi openDbi(final String name, final DbiFlags... flags) { - final byte[] nameBytes = name == null ? null : name.getBytes(UTF_8); - return openDbi(nameBytes, null, false, flags); + return openDbi(Dbi.getNameBytes(name), null, false, flags); } /** - * Convenience method that opens a {@link Dbi} with a UTF-8 database name and associated {@link - * Comparator} that is not invoked from native code. - * * @param name name of the database (or null if no name is required) - * @param comparator custom comparator callback (or null to use default) + * @param comparator custom comparator for cursor start/stop key comparisons. If null, LMDB's + * comparator will be used. * @param flags to open the database with * @return a database that is ready to use + * @deprecated Instead use {@link Env#createDbi()} + *

Convenience method that opens a {@link Dbi} with a UTF-8 database name and associated + * {@link Comparator} for use by {@link CursorIterable} when comparing start/stop keys. + *

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

Convenience method that opens a {@link Dbi} with a UTF-8 database name and associated + * {@link Comparator}. The comparator will be used by {@link CursorIterable} when comparing + * start/stop keys as a minimum. If nativeCb is {@code true}, this comparator will also be + * called by LMDB to determine insertion/iteration order. Calling back to a java comparator + * may significantly impact performance. */ + @Deprecated() public Dbi openDbi( final String name, final Comparator comparator, final boolean nativeCb, final DbiFlags... flags) { - final byte[] nameBytes = name == null ? null : name.getBytes(UTF_8); - return openDbi(nameBytes, comparator, nativeCb, flags); + return openDbi(Dbi.getNameBytes(name), comparator, nativeCb, flags); } /** - * Convenience method that opens a {@link Dbi} with a default {@link Comparator} that is not - * invoked from native code. - * * @param name name of the database (or null if no name is required) * @param flags to open the database with * @return a database that is ready to use + * @deprecated Instead use {@link Env#createDbi()} + *

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

Convenience method that opens a {@link Dbi} with an associated {@link Comparator} that + * is not invoked from native code. */ + @Deprecated() public Dbi openDbi( final byte[] name, final Comparator comparator, final DbiFlags... flags) { return openDbi(name, comparator, false, flags); } /** - * 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 Instead use {@link Env#createDbi()} + *

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. */ + @Deprecated() public Dbi openDbi( final byte[] name, final Comparator comparator, final boolean nativeCb, final DbiFlags... flags) { try (Txn txn = readOnly ? txnRead() : txnWrite()) { - final Dbi dbi = openDbi(txn, name, comparator, nativeCb, flags); + final Dbi dbi = + new Dbi<>(this, txn, name, comparator, nativeCb, proxy, DbiFlagSet.of(flags)); txn.commit(); // even RO Txns require a commit to retain Dbi in Env return dbi; } } /** - * Open the {@link Dbi} using the passed {@link Txn}. - * - *

The caller must commit the transaction after this method returns in order to retain the - * Dbi in the Env. - * - *

A {@link Comparator} may be provided when calling this method. Such comparator is primarily - * used by {@link CursorIterable} instances. A secondary (but uncommon) use of the comparator is - * to act as a callback from the native library if nativeCb is true. - * This is usually avoided due to the overhead of native code calling back into Java. It is - * instead highly recommended to set the correct {@link DbiFlags} to allow the native library to - * correctly order the intended keys. - * - *

A default comparator will be provided if null is passed as the comparator. If a - * custom comparator is provided, it must strictly match the lexicographical order of keys in the - * native LMDB database. - * - *

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 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#createDbi()} + *

Open the {@link Dbi} using the passed {@link Txn}. + *

The caller must commit the transaction after this method returns in order to retain the + * Dbi in the Env. + *

A {@link Comparator} may be provided when calling this method. Such comparator is + * primarily used by {@link CursorIterable} instances. A secondary (but uncommon) use of the + * comparator is to act as a callback from the native library if nativeCb is + * true. This is usually avoided due to the overhead of native code calling back + * into Java. It is instead highly recommended to set the correct {@link DbiFlags} to allow + * the native library to correctly order the intended keys. + *

A default comparator will be provided if null is passed as the comparator. + * If a custom comparator is provided, it must strictly match the lexicographical order of + * keys in the native LMDB database. + *

This method (and its overloaded convenience variants) must not be called from concurrent + * threads. */ + @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); + return new Dbi<>(this, txn, name, comparator, nativeCb, proxy, DbiFlagSet.of(flags)); } /** @@ -410,16 +528,40 @@ public void sync(final boolean force) { } /** - * 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) + * @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) { - if (closed) { - throw new AlreadyClosedException(); - } + 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) + * @return a transaction (never null) + */ + 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 +571,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 +581,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() { @@ -451,22 +595,22 @@ void checkNotClosed() { } } - private void validateDirectoryEmpty(final File path) { - if (!path.exists()) { + private void validateDirectoryEmpty(final Path path) { + if (!Files.exists(path)) { throw new InvalidCopyDestination("Path does not exist"); } - if (!path.isDirectory()) { + if (!Files.isDirectory(path)) { throw new InvalidCopyDestination("Path must be a directory"); } - final String[] files = path.list(); - if (files != null && files.length > 0) { + final long fileCount = FileUtil.count(path); + if (fileCount > 0) { throw new InvalidCopyDestination("Path must contain no files"); } } - private void validatePath(final File path) { + private void validatePath(final Path path) { if (noSubDir) { - if (path.exists()) { + if (Files.exists(path)) { throw new InvalidCopyDestination("Path must not exist for MDB_NOSUBDIR"); } return; @@ -485,6 +629,29 @@ public int readerCheck() { return resultPtr.intValue(); } + /** For testing use. */ + EnvFlagSet getEnvFlagSet() { + return envFlagSet; + } + + @Override + public String toString() { + return "Env{" + + "closed=" + + closed + + ", maxKeySize=" + + maxKeySize + + ", noSubDir=" + + noSubDir + + ", readOnly=" + + readOnly + + ", path=" + + path + + ", envFlagSet=" + + envFlagSet + + '}'; + } + /** Object has already been closed and the operation is therefore prohibited. */ public static final class AlreadyClosedException extends LmdbException { @@ -515,11 +682,17 @@ public AlreadyOpenException() { public static final class Builder { static final int MAX_READERS_DEFAULT = 126; - private long mapSize = 1_024 * 1_024; + static final long MAP_SIZE_DEFAULT = ByteUnit.MEBIBYTES.toBytes(1); + static final int POSIX_MODE_DEFAULT = 0664; + + private long mapSize = MAP_SIZE_DEFAULT; private int maxDbs = 1; private int maxReaders = MAX_READERS_DEFAULT; private boolean opened; private final BufferProxy proxy; + private int mode = POSIX_MODE_DEFAULT; + private final AbstractFlagSet.Builder flagSetBuilder = + EnvFlagSet.builder(); Builder(final BufferProxy proxy) { requireNonNull(proxy); @@ -533,8 +706,50 @@ public static final class Builder { * @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,11 +762,11 @@ 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)); - return new Env<>(proxy, ptr, readOnly, noSubDir); + 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, path, flags); } catch (final LmdbNativeException e) { LIB.mdb_env_close(ptr); throw e; @@ -559,18 +774,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 @@ -586,6 +790,21 @@ public Builder setMapSize(final long mapSize) { return this; } + /** + * Sets the map size in the supplied unit. + * + * @param mapSize new map size in the units of byteUnit. + * @param byteUnit The unit that mapSize is in. + * @return the builder + */ + public Builder setMapSize(final long mapSize, final ByteUnit byteUnit) { + requireNonNull(byteUnit); + if (mapSize < 0) { + throw new IllegalArgumentException("Negative value; overflow?"); + } + return setMapSize(byteUnit.toBytes(mapSize)); + } + /** * Sets the maximum number of databases (ie {@link Dbi}s permitted. * @@ -613,6 +832,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(flagSetBuilder::addFlag); + } + 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::addFlag); + } + 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.setFlags(envFlagSet.getFlags()); + } + return this; + } + + /** + * Adds a single {@link EnvFlags} to any existing flags. + * + * @param envFlag The flag to add to any existing flags. A null value is a no-op. + * @return this builder instance. + */ + public Builder addEnvFlag(final EnvFlags envFlag) { + this.flagSetBuilder.addFlag(envFlag); + return this; + } + + /** + * Adds the contents of an {@link EnvFlagSet} to any existing flags. + * + * @param envFlagSet 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 envFlagSet) { + if (envFlagSet != null) { + flagSetBuilder.addFlags(envFlagSet.getFlags()); + } + return this; + } + + /** + * Adds a {@link Collection} of {@link EnvFlags} to any existing flags. + * + * @param envFlags The {@link Collection} of flags to add to any existing flags. A null value is + * a no-op. + * @return this builder instance. + */ + public Builder addEnvFlags(final Collection envFlags) { + if (envFlags != null) { + flagSetBuilder.addFlags(envFlags); + } + return this; + } } /** File is not a valid LMDB file. */ diff --git a/src/main/java/org/lmdbjava/EnvFlagSet.java b/src/main/java/org/lmdbjava/EnvFlagSet.java new file mode 100644 index 00000000..70ed4e90 --- /dev/null +++ b/src/main/java/org/lmdbjava/EnvFlagSet.java @@ -0,0 +1,76 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import java.util.Collection; +import java.util.Objects; + +/** An immutable set of flags for use when opening the {@link Env}. */ +public interface EnvFlagSet extends FlagSet { + + /** An immutable empty {@link EnvFlagSet}. */ + EnvFlagSet EMPTY = EnvFlagSetImpl.EMPTY; + + /** + * Gets the immutable empty {@link EnvFlagSet} instance. + * + * @return The immutable empty {@link EnvFlagSet} instance. + */ + static EnvFlagSet empty() { + return EnvFlagSetImpl.EMPTY; + } + + /** + * Creates an immutable {@link EnvFlagSet} containing envFlag. + * + * @param envFlag The flag to include in the {@link EnvFlagSet} + * @return An immutable {@link EnvFlagSet} containing just envFlag. + */ + static EnvFlagSet of(final EnvFlags envFlag) { + Objects.requireNonNull(envFlag); + return envFlag; + } + + /** + * Creates an immutable {@link EnvFlagSet} containing envFlags. + * + * @param envFlags The flags to include in the {@link EnvFlagSet}. + * @return An immutable {@link EnvFlagSet} containing envFlags. + */ + static EnvFlagSet of(final EnvFlags... envFlags) { + return builder().setFlags(envFlags).build(); + } + + /** + * Creates an immutable {@link EnvFlagSet} containing envFlags. + * + * @param envFlags The flags to include in the {@link EnvFlagSet}. + * @return An immutable {@link EnvFlagSet} containing envFlags. + */ + static EnvFlagSet of(final Collection envFlags) { + return builder().setFlags(envFlags).build(); + } + + /** + * Create a builder for building an {@link EnvFlagSet}. + * + * @return A builder instance for building an {@link EnvFlagSet}. + */ + static AbstractFlagSet.Builder builder() { + return new AbstractFlagSet.Builder<>( + EnvFlags.class, EnvFlagSetImpl::new, envFlag -> envFlag, () -> EnvFlagSetImpl.EMPTY); + } +} diff --git a/src/main/java/org/lmdbjava/EnvFlagSetEmpty.java b/src/main/java/org/lmdbjava/EnvFlagSetEmpty.java new file mode 100644 index 00000000..552b1646 --- /dev/null +++ b/src/main/java/org/lmdbjava/EnvFlagSetEmpty.java @@ -0,0 +1,19 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +class EnvFlagSetEmpty extends AbstractFlagSet.AbstractEmptyFlagSet + implements EnvFlagSet {} diff --git a/src/main/java/org/lmdbjava/EnvFlagSetImpl.java b/src/main/java/org/lmdbjava/EnvFlagSetImpl.java new file mode 100644 index 00000000..ba1bdfef --- /dev/null +++ b/src/main/java/org/lmdbjava/EnvFlagSetImpl.java @@ -0,0 +1,27 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import java.util.EnumSet; + +class EnvFlagSetImpl extends AbstractFlagSet implements EnvFlagSet { + + static final EnvFlagSet EMPTY = new EnvFlagSetEmpty(); + + EnvFlagSetImpl(final EnumSet flags) { + super(flags); + } +} diff --git a/src/main/java/org/lmdbjava/EnvFlags.java b/src/main/java/org/lmdbjava/EnvFlags.java index 4ce555a8..1d5c9214 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 this == flag; + } + + @Override + public int size() { + return 1; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public String toString() { + return FlagSet.asString(this); + } } diff --git a/src/test/java/org/lmdbjava/FileUtil.java b/src/main/java/org/lmdbjava/FileUtil.java similarity index 74% rename from src/test/java/org/lmdbjava/FileUtil.java rename to src/main/java/org/lmdbjava/FileUtil.java index 3e459f14..476f0436 100644 --- a/src/test/java/org/lmdbjava/FileUtil.java +++ b/src/main/java/org/lmdbjava/FileUtil.java @@ -25,54 +25,13 @@ import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.util.EnumSet; -import java.util.function.Consumer; import java.util.stream.Stream; final class FileUtil { private FileUtil() {} - static Path createTempDir() { - try { - return Files.createTempDirectory("lmdbjava"); - } catch (final IOException e) { - throw new UncheckedIOException(e); - } - } - - static Path createTempFile() { - try { - return Files.createTempFile("lmdbjava", "db"); - } catch (final IOException e) { - throw new UncheckedIOException(e); - } - } - - static void useTempDir(final Consumer consumer) { - Path path = null; - try { - path = createTempDir(); - consumer.accept(path); - } finally { - if (path != null) { - deleteDir(path); - } - } - } - - static void useTempFile(final Consumer consumer) { - Path path = null; - try { - path = createTempFile(); - consumer.accept(path); - } finally { - if (path != null) { - deleteIfExists(path); - } - } - } - - public static long size(final Path path) { + static long size(final Path path) { try { return Files.size(path); } catch (final IOException e) { @@ -80,7 +39,7 @@ public static long size(final Path path) { } } - public static void delete(final Path path) { + static void deleteFile(final Path path) { try { Files.delete(path); } catch (final IOException e) { @@ -88,7 +47,7 @@ public static void delete(final Path path) { } } - public static void deleteDir(final Path path) { + static void deleteDir(final Path path) { if (path != null && Files.isDirectory(path)) { recursiveDelete(path); deleteIfExists(path); @@ -143,7 +102,7 @@ private static void deleteIfExists(final Path path) { } } - public static long count(final Path path) { + static long count(final Path path) { try (final Stream stream = Files.list(path)) { return stream.count(); } catch (final IOException e) { diff --git a/src/main/java/org/lmdbjava/FlagSet.java b/src/main/java/org/lmdbjava/FlagSet.java new file mode 100644 index 00000000..bec120e4 --- /dev/null +++ b/src/main/java/org/lmdbjava/FlagSet.java @@ -0,0 +1,151 @@ +/* + * 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 The type of flag in the set, must extend {@link MaskedFlag}. + */ +public interface FlagSet extends Iterable { + + /** + * The combined mask for this flagSet. + * + * @return The combined mask for this flagSet. + */ + int getMask(); + + /** + * Combines this {@link FlagSet} with another and returns the combined mask value. + * + * @param other The other {@link FlagSet} to combine with this. + * @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(); + } + } + + /** + * Get the set of flags in this {@link FlagSet}. + * + * @return The set of flags in this {@link FlagSet}. + */ + Set getFlags(); + + /** + * Tests if flag is non-null and included in this {@link FlagSet}. + * + * @param flag The flag to test. + * @return True if flag is non-null and included in this {@link FlagSet}. + */ + boolean isSet(T flag); + + /** + * The number of flags in this set. + * + * @return The number of flags in this set. + */ + int size(); + + /** + * Tests if at least one of flags are included in this {@link FlagSet} + * + * @param flags The flags to test. + * @return True if at least one of flags are included in this {@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; + } + + /** + * Tests if this {@link FlagSet} is empty. + * + * @return True if this {@link FlagSet} is empty. + */ + boolean isEmpty(); + + /** + * Gets an {@link Iterator} (in no particular order) for the flags in this {@link FlagSet}. + * + * @return The {@link Iterator} (in no particular order) for the flags in this {@link FlagSet}. + */ + @Override + default Iterator iterator() { + return getFlags().iterator(); + } + + /** + * Convert this {@link FlagSet} to a string for use in toString methods. + * + * @param flagSet The {@link FlagSet} to convert to a string. + * @param The type of the flags in the {@link FlagSet}. + * @return The {@link String} representation of the flagSet. + */ + 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() + '}'; + } + + /** + * Compares a {@link FlagSet} to another object + * + * @param flagSet The {@link FlagSet} to compare. + * @param other THe object to compare against the {@link FlagSet}. + * @return True if both arguments implement {@link FlagSet} and contain the same flags. + */ + 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..da8bbe6b --- /dev/null +++ b/src/main/java/org/lmdbjava/Key.java @@ -0,0 +1,61 @@ +/* + * 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. Equivalent to {@link KeyVal} without the val part. + * + * @param buffer type + */ +final class Key implements AutoCloseable { + + private static final MemoryManager MEM_MGR = RUNTIME.getMemoryManager(); + private boolean closed; + private final T k; + private final BufferProxy proxy; + private final Pointer ptrKey; + + Key(final BufferProxy proxy) { + requireNonNull(proxy); + this.proxy = proxy; + this.k = proxy.allocate(); + ptrKey = MEM_MGR.allocateTemporary(MDB_VAL_STRUCT_SIZE, false); + } + + @Override + public void close() { + if (closed) { + return; + } + closed = true; + proxy.deallocate(k); + } + + void keyIn(final T key) { + proxy.in(key, ptrKey); + } + + Pointer pointer() { + return ptrKey; + } +} diff --git a/src/main/java/org/lmdbjava/KeyRangeType.java b/src/main/java/org/lmdbjava/KeyRangeType.java index ad67286d..4cb1cc9b 100644 --- a/src/main/java/org/lmdbjava/KeyRangeType.java +++ b/src/main/java/org/lmdbjava/KeyRangeType.java @@ -46,7 +46,7 @@ public enum KeyRangeType { * *

In our example, the returned keys would be 2, 4, 6 and 8. */ - FORWARD_ALL(true, false, false), + FORWARD_ALL(true, false, false, false, false), /** * Start on the passed key (or the first key immediately after it) and iterate forward until no * keys remain. @@ -56,7 +56,7 @@ public enum KeyRangeType { *

In our example and with a passed search key of 5, the returned keys would be 6 and 8. With a * passed key of 6, the returned keys would be 6 and 8. */ - FORWARD_AT_LEAST(true, true, false), + FORWARD_AT_LEAST(true, true, true, false, false), /** * Start on the first key and iterate forward until a key equal to it (or the first key * immediately after it) is reached. @@ -66,7 +66,7 @@ public enum KeyRangeType { *

In our example and with a passed search key of 5, the returned keys would be 2 and 4. With a * passed key of 6, the returned keys would be 2, 4 and 6. */ - FORWARD_AT_MOST(true, false, true), + FORWARD_AT_MOST(true, false, false, true, true), /** * Iterate forward between the passed keys, matching on the first keys directly equal to the * passed key (or immediately following it in the case of the "start" key, or immediately @@ -77,7 +77,7 @@ public enum KeyRangeType { *

In our example and with a passed search range of 3 - 7, the returned keys would be 4 and 6. * With a range of 2 - 6, the keys would be 2, 4 and 6. */ - FORWARD_CLOSED(true, true, true), + FORWARD_CLOSED(true, true, true, true, true), /** * Iterate forward between the passed keys, matching on the first keys directly equal to the * passed key (or immediately following it in the case of the "start" key, or immediately @@ -88,7 +88,7 @@ public enum KeyRangeType { *

In our example and with a passed search range of 3 - 8, the returned keys would be 4 and 6. * With a range of 2 - 6, the keys would be 2 and 4. */ - FORWARD_CLOSED_OPEN(true, true, true), + FORWARD_CLOSED_OPEN(true, true, true, true, false), /** * Start after the passed key (but not equal to it) and iterate forward until no keys remain. * @@ -97,7 +97,7 @@ public enum KeyRangeType { *

In our example and with a passed search key of 4, the returned keys would be 6 and 8. With a * passed key of 3, the returned keys would be 4, 6 and 8. */ - FORWARD_GREATER_THAN(true, true, false), + FORWARD_GREATER_THAN(true, true, false, false, false), /** * Start on the first key and iterate forward until a key the passed key has been reached (but do * not return that key). @@ -107,7 +107,7 @@ public enum KeyRangeType { *

In our example and with a passed search key of 5, the returned keys would be 2 and 4. With a * passed key of 8, the returned keys would be 2, 4 and 6. */ - FORWARD_LESS_THAN(true, false, true), + FORWARD_LESS_THAN(true, false, false, true, false), /** * Iterate forward between the passed keys but not equal to either of them. * @@ -116,7 +116,7 @@ public enum KeyRangeType { *

In our example and with a passed search range of 3 - 7, the returned keys would be 4 and 6. * With a range of 2 - 8, the key would be 4 and 6. */ - FORWARD_OPEN(true, true, true), + FORWARD_OPEN(true, true, false, true, false), /** * Iterate forward between the passed keys. Do not return the "start" key, but do return the * "stop" key. @@ -126,7 +126,7 @@ public enum KeyRangeType { *

In our example and with a passed search range of 3 - 8, the returned keys would be 4, 6 and * 8. With a range of 2 - 6, the keys would be 4 and 6. */ - FORWARD_OPEN_CLOSED(true, true, true), + FORWARD_OPEN_CLOSED(true, true, false, true, true), /** * Start on the last key and iterate backward until no keys remain. * @@ -134,7 +134,7 @@ public enum KeyRangeType { * *

In our example, the returned keys would be 8, 6, 4 and 2. */ - BACKWARD_ALL(false, false, false), + BACKWARD_ALL(false, false, false, false, false), /** * Start on the passed key (or the first key immediately preceding it) and iterate backward until * no keys remain. @@ -145,7 +145,7 @@ public enum KeyRangeType { * passed key of 6, the returned keys would be 6, 4 and 2. With a passed key of 9, the returned * keys would be 8, 6, 4 and 2. */ - BACKWARD_AT_LEAST(false, true, false), + BACKWARD_AT_LEAST(false, true, true, false, false), /** * Start on the last key and iterate backward until a key equal to it (or the first key * immediately preceding it it) is reached. @@ -155,7 +155,7 @@ public enum KeyRangeType { *

In our example and with a passed search key of 5, the returned keys would be 8 and 6. With a * passed key of 6, the returned keys would be 8 and 6. */ - BACKWARD_AT_MOST(false, false, true), + BACKWARD_AT_MOST(false, false, false, true, true), /** * Iterate backward between the passed keys, matching on the first keys directly equal to the * passed key (or immediately preceding it in the case of the "start" key, or immediately @@ -167,7 +167,7 @@ public enum KeyRangeType { * With a range of 6 - 2, the keys would be 6, 4 and 2. With a range of 9 - 3, the returned keys * would be 8, 6 and 4. */ - BACKWARD_CLOSED(false, true, true), + BACKWARD_CLOSED(false, true, true, true, true), /** * Iterate backward between the passed keys, matching on the first keys directly equal to the * passed key (or immediately preceding it in the case of the "start" key, or immediately @@ -179,7 +179,7 @@ public enum KeyRangeType { * 4. With a range of 7 - 2, the keys would be 6 and 4. With a range of 9 - 3, the keys would be * 8, 6 and 4. */ - BACKWARD_CLOSED_OPEN(false, true, true), + BACKWARD_CLOSED_OPEN(false, true, true, true, false), /** * Start immediate prior to the passed key (but not equal to it) and iterate backward until no * keys remain. @@ -190,7 +190,7 @@ public enum KeyRangeType { * passed key of 7, the returned keys would be 6, 4 and 2. With a passed key of 9, the returned * keys would be 8, 6, 4 and 2. */ - BACKWARD_GREATER_THAN(false, true, false), + BACKWARD_GREATER_THAN(false, true, false, false, false), /** * Start on the last key and iterate backward until the last key greater than the passed "stop" * key is reached. Do not return the "stop" key. @@ -200,7 +200,7 @@ public enum KeyRangeType { *

In our example and with a passed search key of 5, the returned keys would be 8 and 6. With a * passed key of 2, the returned keys would be 8, 6 and 4 */ - BACKWARD_LESS_THAN(false, false, true), + BACKWARD_LESS_THAN(false, false, false, true, false), /** * Iterate backward between the passed keys, but do not return the passed keys. * @@ -210,7 +210,7 @@ public enum KeyRangeType { * With a range of 8 - 1, the keys would be 6, 4 and 2. With a range of 9 - 4, the keys would be 8 * and 6. */ - BACKWARD_OPEN(false, true, true), + BACKWARD_OPEN(false, true, false, true, false), /** * Iterate backward between the passed keys. Do not return the "start" key, but do return the * "stop" key. @@ -221,19 +221,25 @@ public enum KeyRangeType { * 2. With a range of 8 - 4, the keys would be 6 and 4. With a range of 9 - 4, the keys would be * 8, 6 and 4. */ - BACKWARD_OPEN_CLOSED(false, true, true); + BACKWARD_OPEN_CLOSED(false, true, false, true, true); private final boolean directionForward; private final boolean startKeyRequired; + private final boolean startKeyInclusive; private final boolean stopKeyRequired; + private final boolean stopKeyInclusive; KeyRangeType( final boolean directionForward, final boolean startKeyRequired, - final boolean stopKeyRequired) { + final boolean startKeyInclusive, + final boolean stopKeyRequired, + final boolean stopKeyInclusive) { this.directionForward = directionForward; this.startKeyRequired = startKeyRequired; + this.startKeyInclusive = startKeyInclusive; this.stopKeyRequired = stopKeyRequired; + this.stopKeyInclusive = stopKeyInclusive; } /** @@ -254,6 +260,16 @@ public boolean isStartKeyRequired() { return startKeyRequired; } + /** + * Is the start key to be treated as inclusive in the range. + * + * @return true if start key is inclusive. False if not inclusive or no start key is required by + * the range type. + */ + public boolean isStartKeyInclusive() { + return startKeyInclusive; + } + /** * Whether the iteration requires a "stop" key. * @@ -263,6 +279,16 @@ public boolean isStopKeyRequired() { return stopKeyRequired; } + /** + * Is the stop key to be treated as inclusive in the range. + * + * @return true if stop key is inclusive. False if not inclusive or no stop key is required by the + * range type. + */ + public boolean isStopKeyInclusive() { + return stopKeyInclusive; + } + /** * Determine the iterator action to take when iterator first begins. * @@ -319,15 +345,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 +361,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..67afebce 100644 --- a/src/main/java/org/lmdbjava/MaskedFlag.java +++ b/src/main/java/org/lmdbjava/MaskedFlag.java @@ -17,14 +17,14 @@ 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 { + /** The mask value for an empty mask, i.e. no flags set. */ + int EMPTY_MASK = 0; + /** * Obtains the integer value for this enum which can be included in a mask. * @@ -33,13 +33,11 @@ public interface MaskedFlag { int getMask(); /** - * Indicates if the flag must be propagated to the underlying C code of LMDB or not. + * The name of the flag. * - * @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 +48,51 @@ 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. + * Combine the two masks into a single mask value, i.e. when combining two {@link FlagSet}s. * - * @param flag type - * @param flags to mask (null or empty returns zero) - * @return the integer mask for use in C + * @param mask1 The mask to combine with mask2. + * @param mask2 The mask to combine with mask1. + * @return The combined mask value for the two passed masks. */ - 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..a5605d1a --- /dev/null +++ b/src/main/java/org/lmdbjava/PutFlagSet.java @@ -0,0 +1,76 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import java.util.Collection; +import java.util.Objects; + +/** An immutable set of flags for use when performing a "put". */ +public interface PutFlagSet extends FlagSet { + + /** An immutable empty {@link PutFlagSet}. */ + PutFlagSet EMPTY = PutFlagSetImpl.EMPTY; + + /** + * Gets the immutable empty {@link PutFlagSet} instance. + * + * @return The immutable empty {@link PutFlagSet} instance. + */ + static PutFlagSet empty() { + return PutFlagSetImpl.EMPTY; + } + + /** + * Creates an immutable {@link PutFlagSet} containing putFlag. + * + * @param putFlag The flag to include in the {@link PutFlagSet} + * @return An immutable {@link PutFlagSet} containing just putFlag. + */ + static PutFlagSet of(final PutFlags putFlag) { + Objects.requireNonNull(putFlag); + return putFlag; + } + + /** + * Creates an immutable {@link PutFlagSet} containing putFlags. + * + * @param putFlags The flags to include in the {@link PutFlagSet}. + * @return An immutable {@link PutFlagSet} containing putFlags. + */ + static PutFlagSet of(final PutFlags... putFlags) { + return builder().setFlags(putFlags).build(); + } + + /** + * Creates an immutable {@link PutFlagSet} containing putFlags. + * + * @param putFlags The flags to include in the {@link PutFlagSet}. + * @return An immutable {@link PutFlagSet} containing putFlags. + */ + static PutFlagSet of(final Collection putFlags) { + return builder().setFlags(putFlags).build(); + } + + /** + * Create a builder for building an {@link PutFlagSet}. + * + * @return A builder instance for building an {@link PutFlagSet}. + */ + static AbstractFlagSet.Builder builder() { + return new AbstractFlagSet.Builder<>( + PutFlags.class, PutFlagSetImpl::new, putFlag -> putFlag, PutFlagSetEmpty::new); + } +} diff --git a/src/main/java/org/lmdbjava/PutFlagSetEmpty.java b/src/main/java/org/lmdbjava/PutFlagSetEmpty.java new file mode 100644 index 00000000..a017711a --- /dev/null +++ b/src/main/java/org/lmdbjava/PutFlagSetEmpty.java @@ -0,0 +1,19 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +class PutFlagSetEmpty extends AbstractFlagSet.AbstractEmptyFlagSet + implements PutFlagSet {} diff --git a/src/main/java/org/lmdbjava/PutFlagSetImpl.java b/src/main/java/org/lmdbjava/PutFlagSetImpl.java new file mode 100644 index 00000000..afa495ef --- /dev/null +++ b/src/main/java/org/lmdbjava/PutFlagSetImpl.java @@ -0,0 +1,27 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import java.util.EnumSet; + +class PutFlagSetImpl extends AbstractFlagSet implements PutFlagSet { + + public static final PutFlagSet EMPTY = new PutFlagSetEmpty(); + + PutFlagSetImpl(final EnumSet flags) { + super(flags); + } +} diff --git a/src/main/java/org/lmdbjava/PutFlags.java b/src/main/java/org/lmdbjava/PutFlags.java index 809103de..2c400bae 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 this == flag; + } + + @Override + public int size() { + return 1; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public String toString() { + return FlagSet.asString(this); + } } diff --git a/src/main/java/org/lmdbjava/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/TargetName.java b/src/main/java/org/lmdbjava/TargetName.java index e3bb5db0..49a65dea 100644 --- a/src/main/java/org/lmdbjava/TargetName.java +++ b/src/main/java/org/lmdbjava/TargetName.java @@ -18,6 +18,11 @@ import static java.lang.System.getProperty; import static java.util.Locale.ENGLISH; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.stream.Stream; + /** * Determines the name of the target LMDB native library. * @@ -129,10 +134,12 @@ private static String err(final String reason) { return reason + " (please set system property " + LMDB_NATIVE_LIB_PROP - + " to the path of an external LMDB native library or property " + + " to the path of an external LMDB native library," + + " or simply 'lmdb' if LMDB is installed in standard system paths;" + + " alternatively set property " + LMDB_EMBEDDED_LIB_PROP - + " to the name of an LmdbJava embedded" - + " library; os.arch='" + + " to the name of an LmdbJava embedded library;" + + " os.arch='" + ARCH + "' os.name='" + OS @@ -160,6 +167,17 @@ private static String resolveOs(final String os) { } private static String resolveToolchain(final String os) { - return check(os, "Mac OS") ? "none" : "gnu"; + if (check(os, "Mac OS")) { + return "none"; + } + return isMuslLibc() ? "musl" : "gnu"; + } + + private static boolean isMuslLibc() { + try (Stream lines = Files.lines(Paths.get("/proc/self/maps"))) { + return lines.anyMatch(line -> line.contains("/ld-musl")); + } catch (final IOException e) { + return false; + } } } diff --git a/src/main/java/org/lmdbjava/Txn.java b/src/main/java/org/lmdbjava/Txn.java index 05e8ce06..7e9aacf9 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; @@ -29,6 +27,7 @@ import static org.lmdbjava.Txn.State.RESET; import static org.lmdbjava.TxnFlags.MDB_RDONLY_TXN; +import java.util.Objects; import jnr.ffi.Pointer; /** @@ -46,11 +45,13 @@ public final class Txn implements AutoCloseable { private final Env env; 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) { + if (SHOULD_CHECK) { + Objects.requireNonNull(flags); + } this.proxy = proxy; this.keyVal = proxy.keyVal(); - final int flagsMask = mask(true, flags); - this.readOnly = isSet(flagsMask, MDB_RDONLY_TXN); + this.readOnly = 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, 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..44beb4e3 --- /dev/null +++ b/src/main/java/org/lmdbjava/TxnFlagSet.java @@ -0,0 +1,76 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import java.util.Collection; +import java.util.Objects; + +/** An immutable set of flags for use when creating a {@link Txn}. */ +public interface TxnFlagSet extends FlagSet { + + /** An immutable empty {@link TxnFlagSet}. */ + TxnFlagSet EMPTY = TxnFlagSetImpl.EMPTY; + + /** + * Gets the immutable empty {@link TxnFlagSet} instance. + * + * @return The immutable empty {@link TxnFlagSet} instance. + */ + static TxnFlagSet empty() { + return TxnFlagSetImpl.EMPTY; + } + + /** + * Creates an immutable {@link TxnFlagSet} containing txnFlag. + * + * @param txnFlag The flag to include in the {@link TxnFlagSet} + * @return An immutable {@link TxnFlagSet} containing just txnFlag. + */ + static TxnFlagSet of(final TxnFlags txnFlag) { + Objects.requireNonNull(txnFlag); + return txnFlag; + } + + /** + * Creates an immutable {@link TxnFlagSet} containing txnFlags. + * + * @param txnFlags The flags to include in the {@link TxnFlagSet}. + * @return An immutable {@link TxnFlagSet} containing txnFlags. + */ + static TxnFlagSet of(final TxnFlags... txnFlags) { + return builder().setFlags(txnFlags).build(); + } + + /** + * Creates an immutable {@link TxnFlagSet} containing txnFlags. + * + * @param txnFlags The flags to include in the {@link TxnFlagSet}. + * @return An immutable {@link TxnFlagSet} containing txnFlags. + */ + static TxnFlagSet of(final Collection txnFlags) { + return builder().setFlags(txnFlags).build(); + } + + /** + * Create a builder for building an {@link TxnFlagSet}. + * + * @return A builder instance for building an {@link TxnFlagSet}. + */ + static AbstractFlagSet.Builder builder() { + return new AbstractFlagSet.Builder<>( + TxnFlags.class, TxnFlagSetImpl::new, txnFlag -> txnFlag, () -> TxnFlagSetImpl.EMPTY); + } +} diff --git a/src/main/java/org/lmdbjava/TxnFlagSetEmpty.java b/src/main/java/org/lmdbjava/TxnFlagSetEmpty.java new file mode 100644 index 00000000..2c229db9 --- /dev/null +++ b/src/main/java/org/lmdbjava/TxnFlagSetEmpty.java @@ -0,0 +1,19 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +class TxnFlagSetEmpty extends AbstractFlagSet.AbstractEmptyFlagSet + implements TxnFlagSet {} diff --git a/src/main/java/org/lmdbjava/TxnFlagSetImpl.java b/src/main/java/org/lmdbjava/TxnFlagSetImpl.java new file mode 100644 index 00000000..c81f3be0 --- /dev/null +++ b/src/main/java/org/lmdbjava/TxnFlagSetImpl.java @@ -0,0 +1,27 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import java.util.EnumSet; + +class TxnFlagSetImpl extends AbstractFlagSet implements TxnFlagSet { + + static final TxnFlagSet EMPTY = new TxnFlagSetEmpty(); + + TxnFlagSetImpl(final EnumSet flags) { + super(flags); + } +} diff --git a/src/main/java/org/lmdbjava/TxnFlags.java b/src/main/java/org/lmdbjava/TxnFlags.java index 26caf6f1..866e1f65 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 this == flag; + } + + @Override + public int size() { + return 1; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public String toString() { + return FlagSet.asString(this); + } } diff --git a/src/main/java/org/lmdbjava/Verifier.java b/src/main/java/org/lmdbjava/Verifier.java index ff9b28f8..87351fb0 100644 --- a/src/main/java/org/lmdbjava/Verifier.java +++ b/src/main/java/org/lmdbjava/Verifier.java @@ -175,7 +175,8 @@ private void createDbis() { private void deleteDbis() { for (final byte[] existingDbiName : env.getDbiNames()) { - final Dbi existingDbi = env.openDbi(existingDbiName); + final Dbi existingDbi = + env.createDbi().setDbName(existingDbiName).withDefaultComparator().open(); try (Txn txn = env.txnWrite()) { existingDbi.drop(txn, true); txn.commit(); diff --git a/src/test/java/org/lmdbjava/AbstractFlagSetTest.java b/src/test/java/org/lmdbjava/AbstractFlagSetTest.java new file mode 100644 index 00000000..a886d9e1 --- /dev/null +++ b/src/test/java/org/lmdbjava/AbstractFlagSetTest.java @@ -0,0 +1,184 @@ +/* + * 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.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +public abstract class AbstractFlagSetTest< + T extends Enum & MaskedFlag & FlagSet, F extends FlagSet> { + + abstract List getAllFlags(); + + abstract F getEmptyFlagSet(); + + abstract AbstractFlagSet.Builder getBuilder(); + + abstract F getFlagSet(final Collection flags); + + abstract F getFlagSet(final T[] flags); + + abstract F getFlagSet(final T flag); + + abstract Class getFlagType(); + + abstract Function, F> getConstructor(); + + T getFirst() { + return getAllFlags().get(0); + } + + @Test + void testEmpty() { + final F emptyFlagSet = getEmptyFlagSet(); + assertThat(emptyFlagSet.getMask()).isEqualTo(0); + assertThat(emptyFlagSet.getFlags()).isEmpty(); + assertThat(emptyFlagSet.isEmpty()).isTrue(); + assertThat(emptyFlagSet.size()).isEqualTo(0); + assertThat(emptyFlagSet.isSet(getFirst())).isFalse(); + assertThat(getBuilder().build().getFlags()).isEqualTo(emptyFlagSet.getFlags()); + } + + @Test + void testSingleFlagSet() { + final List allFlags = getAllFlags(); + for (T flag : allFlags) { + final F flagSet = getBuilder().addFlag(flag).build(); + assertThat(flagSet.getMask()).isEqualTo(flag.getMask()); + assertThat(flagSet.getMask()).isEqualTo(MaskedFlag.mask(flag)); + assertThat(flagSet.getFlags()).containsExactly(flag); + assertThat(flagSet.size()).isEqualTo(1); + assertThat(FlagSet.equals(flagSet, new Object())).isFalse(); + assertThat(FlagSet.equals(flagSet, null)).isFalse(); + assertThat(FlagSet.equals(flag, flag)).isTrue(); + assertThat(FlagSet.equals(flagSet, flag)).isTrue(); + assertThat(FlagSet.equals(flagSet, getFlagSet(flag))).isTrue(); + assertThat(FlagSet.equals(flagSet, getFlagSet(flagSet.getFlags()))).isTrue(); + assertThat(flagSet.areAnySet(flag)).isTrue(); + assertThat(flagSet.areAnySet(null)).isFalse(); + assertThat(flagSet.areAnySet(getEmptyFlagSet())).isFalse(); + assertThat(flagSet.isSet(null)).isFalse(); + assertThat(flagSet.isSet(getFirst())).isEqualTo(getFirst() == flag); + if (getFirst() == flag) { + assertThat(flagSet.getMask()).isEqualTo(MaskedFlag.mask(getFirst())); + } else { + assertThat(flagSet.getMask()).isNotEqualTo(MaskedFlag.mask(getFirst())); + assertThat(flagSet.getMaskWith(getFirst())).isEqualTo(MaskedFlag.mask(flag, getFirst())); + } + assertThat(flagSet.toString()).isNotNull(); + assertThat(flag.name()).isNotNull(); + assertThat(flag.isSet(flag)).isTrue(); + assertThat(flag.isSet(null)).isFalse(); + assertThat(flagSet.getMaskWith(null)).isEqualTo(flagSet.getMask()); + assertThat(flag.isEmpty()).isFalse(); + assertThat(flag.size()).isEqualTo(1); + + assertThat(flag.getFlags()).containsExactlyElementsOf(getFlagSet(flag).getFlags()); + assertThat(flag.getFlags()).hasSize(1); + assertThat(flag.getMask()).isEqualTo(getFlagSet(flag).getMask()); + } + } + + @Test + void testAllFlags() { + final List allFlags = getAllFlags(); + final List flags = new ArrayList<>(allFlags.size()); + final Set masks = new HashSet<>(); + final T firstFlag = getFirst(); + for (T flag : allFlags) { + flags.add(flag); + final F flagSet = getBuilder().setFlags(flags).build(); + final int flagSetMask = flagSet.getMask(); + + // Make sure all the mask values are unique + assertThat(masks).doesNotContain(flagSetMask); + masks.add(flagSetMask); + assertThat(flagSetMask).isEqualTo(MaskedFlag.mask(flags)); + final T[] flagsArr = flags.stream().toArray(this::toArray); + assertThat(flagSetMask).isEqualTo(MaskedFlag.mask(flagsArr)); + assertThat(flagSet.getFlags()).containsExactlyElementsOf(flags); + assertThat(flagSet).isNotEmpty(); + assertThat(FlagSet.equals(flagSet, getBuilder().setFlags(flagsArr).build())).isTrue(); + assertThat(FlagSet.equals(flagSet, getFlagSet(flags))).isTrue(); + assertThat(FlagSet.equals(flagSet, getFlagSet(flagsArr))).isTrue(); + assertThat(flagSet.size()).isEqualTo(flags.size()); + assertThat(flagSet.isSet(getFirst())).isEqualTo(true); + + final int maskWith = flagSet.getMaskWith(firstFlag); + final List combinedList = new ArrayList<>(flags); + combinedList.add(firstFlag); + assertThat(maskWith).isEqualTo(MaskedFlag.mask(combinedList)); + } + } + + /** Test as an enum instance rather than a {@link FlagSet} */ + @Test + void testAsFlag() { + final T flag = getFirst(); + assertThat(flag.size()).isEqualTo(1); + assertThat(flag.getFlags()).hasSize(1); + final T flag2 = flag.getFlags().iterator().next(); + assertThat(flag2 == flag).isTrue(); + assertThat(flag.getMask()).isEqualTo(MaskedFlag.mask(flag)); + assertThat(flag.isEmpty()).isFalse(); + assertThat(flag.toString()).isNotNull(); + assertThat(flag.isSet(flag)).isTrue(); + assertThat(flag.isSet(flag2)).isTrue(); + assertThat(flag.isSet(null)).isFalse(); + final List allFlags = getAllFlags(); + if (allFlags.size() > 1) { + T secondFlag = allFlags.get(1); + assertThat(flag.isSet(secondFlag)).isFalse(); + } + } + + @Test + void testAddCollection() { + final F flagSet = getBuilder().addFlags(getAllFlags()).build(); + + assertThat(flagSet.getFlags()).containsExactlyElementsOf(getAllFlags()); + } + + @Test + void testClearBuilder() { + final F flagSet = getBuilder().addFlag(getFirst()).clear().build(); + + assertThat(flagSet.isEmpty()).isTrue(); + } + + @Test + void testConstructor() { + final Function, F> constructor = getConstructor(); + EnumSet set = EnumSet.allOf(getFlagType()); + final F flagSet = constructor.apply(set); + Assertions.assertThat(flagSet.getFlags()).containsExactlyInAnyOrderElementsOf(getAllFlags()); + } + + private T[] toArray(final int cnt) { + //noinspection unchecked + return (T[]) Array.newInstance(getFlagType(), cnt); + } +} diff --git a/src/test/java/org/lmdbjava/ByteBufProxyTest.java b/src/test/java/org/lmdbjava/ByteBufProxyTest.java new file mode 100644 index 00000000..92a6b493 --- /dev/null +++ b/src/test/java/org/lmdbjava/ByteBufProxyTest.java @@ -0,0 +1,165 @@ +/* + * 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 io.netty.buffer.ByteBuf; +import io.netty.buffer.PooledByteBufAllocator; +import java.nio.ByteOrder; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Random; +import java.util.Set; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class ByteBufProxyTest { + + @Test + public void verifyComparators_int() { + final Random random = new Random(203948); + final ByteBufProxy byteBufProxy = new ByteBufProxy(PooledByteBufAllocator.DEFAULT); + final ByteBuf buffer1native = byteBufProxy.allocate().capacity(Integer.BYTES); + final ByteBuf buffer2native = byteBufProxy.allocate().capacity(Integer.BYTES); + final ByteBuf buffer1be = byteBufProxy.allocate().capacity(Integer.BYTES); + final ByteBuf buffer2be = byteBufProxy.allocate().capacity(Integer.BYTES); + final int[] values = random.ints().filter(i -> i >= 0).limit(5_000_000).toArray(); + + final LinkedHashMap> comparators = new LinkedHashMap<>(); + comparators.put(CompareType.INTEGER_KEY, ByteBufProxy::compareAsIntegerKeys); + comparators.put(CompareType.LEXICOGRAPHIC, ByteBufProxy::compareLexicographically); + + final LinkedHashMap results = + new LinkedHashMap<>(comparators.size()); + final Set uniqueResults = new HashSet<>(comparators.size()); + + for (int i = 1; i < values.length; i++) { + resetBuffer(buffer1native); + resetBuffer(buffer2native); + resetBuffer(buffer1be); + resetBuffer(buffer2be); + + final int val1 = values[i - 1]; + final int val2 = values[i]; + if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) { + buffer1native.writeIntLE(val1); + buffer2native.writeIntLE(val2); + } else { + buffer1native.writeInt(val1); + buffer2native.writeInt(val2); + } + buffer1be.writeInt(val1); + buffer2be.writeInt(val2); + + Assertions.assertThat(buffer1native.readableBytes()).isEqualTo(Integer.BYTES); + Assertions.assertThat(buffer2native.readableBytes()).isEqualTo(Integer.BYTES); + Assertions.assertThat(buffer1be.readableBytes()).isEqualTo(Integer.BYTES); + Assertions.assertThat(buffer2be.readableBytes()).isEqualTo(Integer.BYTES); + + uniqueResults.clear(); + + // Make sure all comparators give the same result for the same inputs + comparators.forEach( + (compareType, comparator) -> { + final ComparatorResult result; + // IntegerKey comparator expects keys to have been written in native order so need + // different buffers. + if (compareType == CompareType.INTEGER_KEY) { + result = TestUtils.compare(comparator, buffer1native, buffer2native); + } else { + result = TestUtils.compare(comparator, buffer1be, buffer2be); + } + results.put(compareType, result); + uniqueResults.add(result); + }); + + if (uniqueResults.size() != 1) { + Assertions.fail( + "Comparator mismatch for values: " + val1 + " and " + val2 + ". Results: " + results); + } + } + } + + @Test + public void verifyComparators_long() { + final Random random = new Random(203948); + final ByteBufProxy byteBufProxy = new ByteBufProxy(PooledByteBufAllocator.DEFAULT); + final ByteBuf buffer1native = byteBufProxy.allocate().capacity(Long.BYTES); + final ByteBuf buffer2native = byteBufProxy.allocate().capacity(Long.BYTES); + final ByteBuf buffer1be = byteBufProxy.allocate().capacity(Long.BYTES); + final ByteBuf buffer2be = byteBufProxy.allocate().capacity(Long.BYTES); + final long[] values = random.longs().filter(i -> i >= 0).limit(5_000_000).toArray(); + + final LinkedHashMap> comparators = new LinkedHashMap<>(); + comparators.put(CompareType.INTEGER_KEY, ByteBufProxy::compareAsIntegerKeys); + comparators.put(CompareType.LEXICOGRAPHIC, ByteBufProxy::compareLexicographically); + + final LinkedHashMap results = + new LinkedHashMap<>(comparators.size()); + final Set uniqueResults = new HashSet<>(comparators.size()); + + for (int i = 1; i < values.length; i++) { + resetBuffer(buffer1native); + resetBuffer(buffer2native); + resetBuffer(buffer1be); + resetBuffer(buffer2be); + + final long val1 = values[i - 1]; + final long val2 = values[i]; + if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) { + buffer1native.writeLongLE(val1); + buffer2native.writeLongLE(val2); + } else { + buffer1native.writeLong(val1); + buffer2native.writeLong(val2); + } + buffer1be.writeLong(val1); + buffer2be.writeLong(val2); + + Assertions.assertThat(buffer1native.readableBytes()).isEqualTo(Long.BYTES); + Assertions.assertThat(buffer2native.readableBytes()).isEqualTo(Long.BYTES); + Assertions.assertThat(buffer1be.readableBytes()).isEqualTo(Long.BYTES); + Assertions.assertThat(buffer2be.readableBytes()).isEqualTo(Long.BYTES); + + uniqueResults.clear(); + + // Make sure all comparators give the same result for the same inputs + comparators.forEach( + (compareType, comparator) -> { + final ComparatorResult result; + // IntegerKey comparator expects keys to have been written in native order so need + // different buffers. + if (compareType == CompareType.INTEGER_KEY) { + result = TestUtils.compare(comparator, buffer1native, buffer2native); + } else { + result = TestUtils.compare(comparator, buffer1be, buffer2be); + } + results.put(compareType, result); + uniqueResults.add(result); + }); + + if (uniqueResults.size() != 1) { + Assertions.fail( + "Comparator mismatch for values: " + val1 + " and " + val2 + ". Results: " + results); + } + } + } + + private static void resetBuffer(ByteBuf buffer1native) { + buffer1native.resetReaderIndex(); + buffer1native.resetWriterIndex(); + } +} diff --git a/src/test/java/org/lmdbjava/ByteBufferProxyTest.java b/src/test/java/org/lmdbjava/ByteBufferProxyTest.java index 82c0abce..2e4ed823 100644 --- a/src/test/java/org/lmdbjava/ByteBufferProxyTest.java +++ b/src/test/java/org/lmdbjava/ByteBufferProxyTest.java @@ -36,8 +36,18 @@ import java.lang.reflect.Field; import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Path; +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; @@ -51,17 +61,22 @@ public final class ByteBufferProxyTest { 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 - } - }); + try (final TempDir tempDir = new TempDir()) { + final Path dir = tempDir.createTempDir(); + try (Env env = create().setMaxReaders(1).open(dir)) { + final Dbi db = + env.createDbi() + .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); } @@ -131,6 +146,168 @@ void unsafeIsDefault() { assertThat(v.getClass().getSimpleName()).startsWith("Unsafe"); } + @Test + public void comparatorPerformance() { + final Random random = new Random(345098); + 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(10_000_000).toArray(); + final int rounds = 100; + + for (int run = 0; run < 3; run++) { + Instant time = Instant.now(); + // x is to ensure result is used by the jvm + int x = 0; + for (int round = 0; round < rounds; round++) { + 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()) + ", x: " + x); + + time = Instant.now(); + int y = 0; + for (int round = 0; round < rounds; round++) { + 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); + y += result; + } + } + System.out.println( + "compareLexicographically: " + Duration.between(time, Instant.now()) + ", y: " + y); + + assertThat(y).isEqualTo(x); + } + } + + @Test + public void verifyComparators_int() { + final Random random = new Random(203948); + final ByteBuffer buffer1native = + ByteBuffer.allocateDirect(Integer.BYTES).order(ByteOrder.nativeOrder()); + final ByteBuffer buffer2native = + ByteBuffer.allocateDirect(Integer.BYTES).order(ByteOrder.nativeOrder()); + final ByteBuffer buffer1be = ByteBuffer.allocateDirect(Integer.BYTES).order(BIG_ENDIAN); + final ByteBuffer buffer2be = ByteBuffer.allocateDirect(Integer.BYTES).order(BIG_ENDIAN); + buffer1native.limit(Integer.BYTES); + buffer2native.limit(Integer.BYTES); + buffer1be.limit(Integer.BYTES); + buffer2be.limit(Integer.BYTES); + final int[] values = random.ints().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 int val1 = values[i - 1]; + final int val2 = values[i]; + buffer1native.putInt(0, val1); + buffer2native.putInt(0, val2); + buffer1be.putInt(0, val1); + buffer2be.putInt(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); + } + } + } + + @Test + public void verifyComparators_long() { + 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/ByteUnitTest.java b/src/test/java/org/lmdbjava/ByteUnitTest.java new file mode 100644 index 00000000..d6608684 --- /dev/null +++ b/src/test/java/org/lmdbjava/ByteUnitTest.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 org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class ByteUnitTest { + + @Test + void test() { + Assertions.assertThat(ByteUnit.BYTES.toBytes(2)).isEqualTo(2); + + // BYTES + Assertions.assertThat(ByteUnit.BYTES.toBytes(2)).isEqualTo(2L); + Assertions.assertThat(ByteUnit.BYTES.toBytes(0)).isEqualTo(0L); + Assertions.assertThat(ByteUnit.BYTES.getFactor()).isEqualTo(1L); + + // IEC Units + Assertions.assertThat(ByteUnit.KIBIBYTES.toBytes(1)).isEqualTo(1024L); + Assertions.assertThat(ByteUnit.KIBIBYTES.toBytes(2)).isEqualTo(2048L); + Assertions.assertThat(ByteUnit.KIBIBYTES.getFactor()).isEqualTo(1024L); + + Assertions.assertThat(ByteUnit.MEBIBYTES.toBytes(1)).isEqualTo(1048576L); + Assertions.assertThat(ByteUnit.MEBIBYTES.toBytes(2)).isEqualTo(2097152L); + Assertions.assertThat(ByteUnit.MEBIBYTES.getFactor()).isEqualTo(1048576L); + + Assertions.assertThat(ByteUnit.GIBIBYTES.toBytes(1)).isEqualTo(1073741824L); + Assertions.assertThat(ByteUnit.GIBIBYTES.toBytes(2)).isEqualTo(2147483648L); + Assertions.assertThat(ByteUnit.GIBIBYTES.getFactor()).isEqualTo(1073741824L); + + Assertions.assertThat(ByteUnit.TEBIBYTES.toBytes(1)).isEqualTo(1099511627776L); + Assertions.assertThat(ByteUnit.TEBIBYTES.toBytes(2)).isEqualTo(2199023255552L); + Assertions.assertThat(ByteUnit.TEBIBYTES.getFactor()).isEqualTo(1099511627776L); + + Assertions.assertThat(ByteUnit.PEBIBYTES.toBytes(1)).isEqualTo(1125899906842624L); + Assertions.assertThat(ByteUnit.PEBIBYTES.toBytes(2)).isEqualTo(2251799813685248L); + Assertions.assertThat(ByteUnit.PEBIBYTES.getFactor()).isEqualTo(1125899906842624L); + + // SI Units + Assertions.assertThat(ByteUnit.KILOBYTES.toBytes(1)).isEqualTo(1000L); + Assertions.assertThat(ByteUnit.KILOBYTES.toBytes(2)).isEqualTo(2000L); + Assertions.assertThat(ByteUnit.KILOBYTES.getFactor()).isEqualTo(1000L); + + Assertions.assertThat(ByteUnit.MEGABYTES.toBytes(1)).isEqualTo(1000000L); + Assertions.assertThat(ByteUnit.MEGABYTES.toBytes(2)).isEqualTo(2000000L); + Assertions.assertThat(ByteUnit.MEGABYTES.getFactor()).isEqualTo(1000000L); + + Assertions.assertThat(ByteUnit.GIGABYTES.toBytes(1)).isEqualTo(1000000000L); + Assertions.assertThat(ByteUnit.GIGABYTES.toBytes(2)).isEqualTo(2000000000L); + Assertions.assertThat(ByteUnit.GIGABYTES.getFactor()).isEqualTo(1000000000L); + + Assertions.assertThat(ByteUnit.TERABYTES.toBytes(1)).isEqualTo(1000000000000L); + Assertions.assertThat(ByteUnit.TERABYTES.toBytes(2)).isEqualTo(2000000000000L); + Assertions.assertThat(ByteUnit.TERABYTES.getFactor()).isEqualTo(1000000000000L); + + Assertions.assertThat(ByteUnit.PETABYTES.toBytes(1)).isEqualTo(1000000000000000L); + Assertions.assertThat(ByteUnit.PETABYTES.toBytes(2)).isEqualTo(2000000000000000L); + Assertions.assertThat(ByteUnit.PETABYTES.getFactor()).isEqualTo(1000000000000000L); + } +} diff --git a/src/test/java/org/lmdbjava/ComparatorIntegerKeyTest.java b/src/test/java/org/lmdbjava/ComparatorIntegerKeyTest.java new file mode 100644 index 00000000..66aaaede --- /dev/null +++ b/src/test/java/org/lmdbjava/ComparatorIntegerKeyTest.java @@ -0,0 +1,360 @@ +/* + * 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.ComparatorResult.EQUAL_TO; +import static org.lmdbjava.ComparatorResult.GREATER_THAN; +import static org.lmdbjava.ComparatorResult.LESS_THAN; +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; + } + + private ComparatorResult compare( + final ComparatorRunner comparatorRunner, final long o1, final long o2) { + return ComparatorResult.get(comparatorRunner.compare(o1, o2)); + } + + private ComparatorResult compare( + final ComparatorRunner comparatorRunner, final int o1, final int o2) { + return ComparatorResult.get(comparatorRunner.compare(o1, o2)); + } + + @ParameterizedTest + @MethodSource("comparatorProvider") + void testLong(final ComparatorRunner comparator) { + + assertThat(compare(comparator, 0L, 0L)).isEqualTo(EQUAL_TO); + assertThat(compare(comparator, Long.MAX_VALUE, Long.MAX_VALUE)).isEqualTo(EQUAL_TO); + + assertThat(compare(comparator, 0L, 1L)).isEqualTo(LESS_THAN); + assertThat(compare(comparator, 0L, Long.MAX_VALUE)).isEqualTo(LESS_THAN); + assertThat(compare(comparator, 0L, 10L)).isEqualTo(LESS_THAN); + assertThat(compare(comparator, 10L, 100L)).isEqualTo(LESS_THAN); + assertThat(compare(comparator, 10L, 100L)).isEqualTo(LESS_THAN); + assertThat(compare(comparator, 10L, 1000L)).isEqualTo(LESS_THAN); + + assertThat(compare(comparator, 1L, 0L)).isEqualTo(GREATER_THAN); + assertThat(compare(comparator, Long.MAX_VALUE, 0L)).isEqualTo(GREATER_THAN); + } + + @ParameterizedTest + @MethodSource("comparatorProvider") + void testInt(final ComparatorRunner comparator) { + + assertThat(compare(comparator, 0, 0)).isEqualTo(EQUAL_TO); + assertThat(compare(comparator, Integer.MAX_VALUE, Integer.MAX_VALUE)).isEqualTo(EQUAL_TO); + + assertThat(compare(comparator, 0, 1)).isEqualTo(LESS_THAN); + assertThat(compare(comparator, 0, Integer.MAX_VALUE)).isEqualTo(LESS_THAN); + assertThat(compare(comparator, 0, 10)).isEqualTo(LESS_THAN); + assertThat(compare(comparator, 10, 100)).isEqualTo(LESS_THAN); + assertThat(compare(comparator, 10, 100)).isEqualTo(LESS_THAN); + assertThat(compare(comparator, 10, 1000)).isEqualTo(LESS_THAN); + + assertThat(compare(comparator, 1, 0)).isEqualTo(GREATER_THAN); + assertThat(compare(comparator, 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 ComparatorResult result = ComparatorResult.get(runner.compare(long1, long2)); + final ComparatorResult expectedResult = ComparatorResult.get(Long.compare(long1, long2)); + + assertThat(result) + .withFailMessage( + () -> + "Compare mismatch - long1: " + + long1 + + ", long2: " + + long2 + + ", expected: " + + expectedResult + + ", actual: " + + result) + .isEqualTo(expectedResult); + + final ComparatorResult result2 = ComparatorResult.get(runner.compare(long2, long1)); + final 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 ComparatorResult result = ComparatorResult.get(runner.compare(int1, int2)); + final ComparatorResult expectedResult = ComparatorResult.get(Integer.compare(int1, int2)); + + assertThat(result) + .withFailMessage( + () -> + "Compare mismatch for - int1: " + + int1 + + ", int2: " + + int2 + + ", expected: " + + expectedResult + + ", actual: " + + result) + .isEqualTo(expectedResult); + + final ComparatorResult result2 = ComparatorResult.get(runner.compare(int2, int1)); + final 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/ComparatorResult.java b/src/test/java/org/lmdbjava/ComparatorResult.java new file mode 100644 index 00000000..56140752 --- /dev/null +++ b/src/test/java/org/lmdbjava/ComparatorResult.java @@ -0,0 +1,40 @@ +/* + * 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; + +/** Converts an integer result code into its contractual meaning. */ +enum ComparatorResult { + LESS_THAN, + EQUAL_TO, + GREATER_THAN; + + static ComparatorResult get(final int comparatorResult) { + if (comparatorResult == 0) { + return EQUAL_TO; + } + 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; + } + } +} diff --git a/src/test/java/org/lmdbjava/ComparatorTest.java b/src/test/java/org/lmdbjava/ComparatorTest.java index a5e010ab..5a1ddb62 100644 --- a/src/test/java/org/lmdbjava/ComparatorTest.java +++ b/src/test/java/org/lmdbjava/ComparatorTest.java @@ -22,7 +22,9 @@ import static org.lmdbjava.ByteArrayProxy.PROXY_BA; import static org.lmdbjava.ByteBufProxy.PROXY_NETTY; import static org.lmdbjava.ByteBufferProxy.PROXY_OPTIMAL; -import static org.lmdbjava.ComparatorTest.ComparatorResult.*; +import static org.lmdbjava.ComparatorResult.EQUAL_TO; +import static org.lmdbjava.ComparatorResult.GREATER_THAN; +import static org.lmdbjava.ComparatorResult.LESS_THAN; import static org.lmdbjava.DirectBufferProxy.PROXY_DB; import com.google.common.primitives.SignedBytes; @@ -34,9 +36,12 @@ import java.util.stream.Stream; import org.agrona.DirectBuffer; import org.agrona.concurrent.UnsafeBuffer; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.support.ParameterDeclarations; /** Tests comparator functions are consistent across buffers. */ public final class ComparatorTest { @@ -54,16 +59,20 @@ public final class ComparatorTest { private static final byte[] LX = buffer(0); private static final byte[] XX = buffer(); - static Stream comparatorProvider() { - return Stream.of( - Arguments.argumentSet("StringRunner", new StringRunner()), - Arguments.argumentSet("DirectBufferRunner", new DirectBufferRunner()), - Arguments.argumentSet("ByteArrayRunner", new ByteArrayRunner()), - Arguments.argumentSet("UnsignedByteArrayRunner", new UnsignedByteArrayRunner()), - Arguments.argumentSet("ByteBufferRunner", new ByteBufferRunner()), - Arguments.argumentSet("NettyRunner", new NettyRunner()), - Arguments.argumentSet("GuavaUnsignedBytes", new GuavaUnsignedBytes()), - Arguments.argumentSet("GuavaSignedBytes", new GuavaSignedBytes())); + static class MyArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments( + ParameterDeclarations parameters, ExtensionContext context) { + return Stream.of( + Arguments.argumentSet("StringRunner", new StringRunner()), + Arguments.argumentSet("DirectBufferRunner", new DirectBufferRunner()), + Arguments.argumentSet("ByteArrayRunner", new ByteArrayRunner()), + Arguments.argumentSet("UnsignedByteArrayRunner", new UnsignedByteArrayRunner()), + Arguments.argumentSet("ByteBufferRunner", new ByteBufferRunner()), + Arguments.argumentSet("NettyRunner", new NettyRunner()), + Arguments.argumentSet("GuavaUnsignedBytes", new GuavaUnsignedBytes()), + Arguments.argumentSet("GuavaSignedBytes", new GuavaSignedBytes())); + } } private static byte[] buffer(final int... bytes) { @@ -75,54 +84,54 @@ private static byte[] buffer(final int... bytes) { } @ParameterizedTest - @MethodSource("comparatorProvider") + @ArgumentsSource(MyArgumentProvider.class) void atLeastOneBufferHasEightBytes(final ComparatorRunner comparator) { - assertThat(get(comparator.compare(HLLLLLLL, LLLLLLLL))).isEqualTo(GREATER_THAN); - assertThat(get(comparator.compare(LLLLLLLL, HLLLLLLL))).isEqualTo(LESS_THAN); + assertThat(TestUtils.compare(comparator, HLLLLLLL, LLLLLLLL)).isEqualTo(GREATER_THAN); + assertThat(TestUtils.compare(comparator, LLLLLLLL, HLLLLLLL)).isEqualTo(LESS_THAN); - assertThat(get(comparator.compare(LHLLLLLL, LLLLLLLL))).isEqualTo(GREATER_THAN); - assertThat(get(comparator.compare(LLLLLLLL, LHLLLLLL))).isEqualTo(LESS_THAN); + assertThat(TestUtils.compare(comparator, LHLLLLLL, LLLLLLLL)).isEqualTo(GREATER_THAN); + assertThat(TestUtils.compare(comparator, LLLLLLLL, LHLLLLLL)).isEqualTo(LESS_THAN); - assertThat(get(comparator.compare(LLLLLLLL, LLLLLLLX))).isEqualTo(GREATER_THAN); - assertThat(get(comparator.compare(LLLLLLLX, LLLLLLLL))).isEqualTo(LESS_THAN); + assertThat(TestUtils.compare(comparator, LLLLLLLL, LLLLLLLX)).isEqualTo(GREATER_THAN); + assertThat(TestUtils.compare(comparator, LLLLLLLX, LLLLLLLL)).isEqualTo(LESS_THAN); - assertThat(get(comparator.compare(HLLLLLLL, HLLLLLLX))).isEqualTo(GREATER_THAN); - assertThat(get(comparator.compare(HLLLLLLX, HLLLLLLL))).isEqualTo(LESS_THAN); + assertThat(TestUtils.compare(comparator, HLLLLLLL, HLLLLLLX)).isEqualTo(GREATER_THAN); + assertThat(TestUtils.compare(comparator, HLLLLLLX, HLLLLLLL)).isEqualTo(LESS_THAN); - assertThat(get(comparator.compare(HLLLLLLX, LHLLLLLL))).isEqualTo(GREATER_THAN); - assertThat(get(comparator.compare(LHLLLLLL, HLLLLLLX))).isEqualTo(LESS_THAN); + assertThat(TestUtils.compare(comparator, HLLLLLLX, LHLLLLLL)).isEqualTo(GREATER_THAN); + assertThat(TestUtils.compare(comparator, LHLLLLLL, HLLLLLLX)).isEqualTo(LESS_THAN); } @ParameterizedTest - @MethodSource("comparatorProvider") + @ArgumentsSource(MyArgumentProvider.class) void buffersOfTwoBytes(final ComparatorRunner comparator) { - assertThat(get(comparator.compare(LL, XX))).isEqualTo(GREATER_THAN); - assertThat(get(comparator.compare(XX, LL))).isEqualTo(LESS_THAN); + assertThat(TestUtils.compare(comparator, LL, XX)).isEqualTo(GREATER_THAN); + assertThat(TestUtils.compare(comparator, XX, LL)).isEqualTo(LESS_THAN); - assertThat(get(comparator.compare(LL, LX))).isEqualTo(GREATER_THAN); - assertThat(get(comparator.compare(LX, LL))).isEqualTo(LESS_THAN); + assertThat(TestUtils.compare(comparator, LL, LX)).isEqualTo(GREATER_THAN); + assertThat(TestUtils.compare(comparator, LX, LL)).isEqualTo(LESS_THAN); - assertThat(get(comparator.compare(LH, LX))).isEqualTo(GREATER_THAN); - assertThat(get(comparator.compare(LX, HL))).isEqualTo(LESS_THAN); + assertThat(TestUtils.compare(comparator, LH, LX)).isEqualTo(GREATER_THAN); + assertThat(TestUtils.compare(comparator, LX, HL)).isEqualTo(LESS_THAN); - assertThat(get(comparator.compare(HX, LL))).isEqualTo(GREATER_THAN); - assertThat(get(comparator.compare(LH, HX))).isEqualTo(LESS_THAN); + assertThat(TestUtils.compare(comparator, HX, LL)).isEqualTo(GREATER_THAN); + assertThat(TestUtils.compare(comparator, LH, HX)).isEqualTo(LESS_THAN); } @ParameterizedTest - @MethodSource("comparatorProvider") + @ArgumentsSource(MyArgumentProvider.class) void equalBuffers(final ComparatorRunner comparator) { - assertThat(get(comparator.compare(LL, LL))).isEqualTo(EQUAL_TO); - assertThat(get(comparator.compare(HX, HX))).isEqualTo(EQUAL_TO); - assertThat(get(comparator.compare(LH, LH))).isEqualTo(EQUAL_TO); - assertThat(get(comparator.compare(LL, LL))).isEqualTo(EQUAL_TO); - assertThat(get(comparator.compare(LX, LX))).isEqualTo(EQUAL_TO); - - assertThat(get(comparator.compare(HLLLLLLL, HLLLLLLL))).isEqualTo(EQUAL_TO); - assertThat(get(comparator.compare(HLLLLLLX, HLLLLLLX))).isEqualTo(EQUAL_TO); - assertThat(get(comparator.compare(LHLLLLLL, LHLLLLLL))).isEqualTo(EQUAL_TO); - assertThat(get(comparator.compare(LLLLLLLL, LLLLLLLL))).isEqualTo(EQUAL_TO); - assertThat(get(comparator.compare(LLLLLLLX, LLLLLLLX))).isEqualTo(EQUAL_TO); + assertThat(TestUtils.compare(comparator, LL, LL)).isEqualTo(EQUAL_TO); + assertThat(TestUtils.compare(comparator, HX, HX)).isEqualTo(EQUAL_TO); + assertThat(TestUtils.compare(comparator, LH, LH)).isEqualTo(EQUAL_TO); + assertThat(TestUtils.compare(comparator, LL, LL)).isEqualTo(EQUAL_TO); + assertThat(TestUtils.compare(comparator, LX, LX)).isEqualTo(EQUAL_TO); + + assertThat(TestUtils.compare(comparator, HLLLLLLL, HLLLLLLL)).isEqualTo(EQUAL_TO); + assertThat(TestUtils.compare(comparator, HLLLLLLX, HLLLLLLX)).isEqualTo(EQUAL_TO); + assertThat(TestUtils.compare(comparator, LHLLLLLL, LHLLLLLL)).isEqualTo(EQUAL_TO); + assertThat(TestUtils.compare(comparator, LLLLLLLL, LLLLLLLL)).isEqualTo(EQUAL_TO); + assertThat(TestUtils.compare(comparator, LLLLLLLX, LLLLLLLX)).isEqualTo(EQUAL_TO); } /** Tests {@link ByteArrayProxy}. */ @@ -140,7 +149,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); } } @@ -247,22 +256,8 @@ public int compare(final byte[] o1, final byte[] o2) { } } - /** Converts an integer result code into its contractual meaning. */ - enum ComparatorResult { - LESS_THAN, - EQUAL_TO, - GREATER_THAN; - - static ComparatorResult get(final int comparatorResult) { - if (comparatorResult == 0) { - return EQUAL_TO; - } - return comparatorResult < 0 ? LESS_THAN : GREATER_THAN; - } - } - /** Interface that can test a {@link BufferProxy} compare method. */ - private interface ComparatorRunner { + private interface ComparatorRunner extends Comparator { /** * Convert the passed byte arrays into the proxy's relevant buffer type and then invoke the @@ -272,6 +267,7 @@ private interface ComparatorRunner { * @param o2 rhs buffer content * @return as per {@link Comparable} */ + @Override int compare(byte[] o1, byte[] o2); } } diff --git a/src/test/java/org/lmdbjava/CompareType.java b/src/test/java/org/lmdbjava/CompareType.java new file mode 100644 index 00000000..bd40bbf4 --- /dev/null +++ b/src/test/java/org/lmdbjava/CompareType.java @@ -0,0 +1,23 @@ +/* + * 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; + +enum CompareType { + /** int and long keys */ + INTEGER_KEY, + LEXICOGRAPHIC, + ; +} diff --git a/src/test/java/org/lmdbjava/CopyFlagSetTest.java b/src/test/java/org/lmdbjava/CopyFlagSetTest.java new file mode 100644 index 00000000..7606682b --- /dev/null +++ b/src/test/java/org/lmdbjava/CopyFlagSetTest.java @@ -0,0 +1,88 @@ +/* + * 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.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class CopyFlagSetTest extends AbstractFlagSetTest { + + @Test + void test() { + // This is here purely to stop CodeQL moaning that this class is unused. + // All the actual tests are in the superclass + Assertions.assertThat(getAllFlags()).isNotNull(); + } + + @Override + List getAllFlags() { + return Arrays.stream(CopyFlags.values()).collect(Collectors.toList()); + } + + @Override + CopyFlagSet getEmptyFlagSet() { + return CopyFlagSet.empty(); + } + + @Override + AbstractFlagSet.Builder getBuilder() { + return CopyFlagSet.builder(); + } + + @Override + CopyFlagSet getFlagSet(Collection flags) { + return CopyFlagSet.of(flags); + } + + @Override + CopyFlagSet getFlagSet(CopyFlags[] flags) { + return CopyFlagSet.of(flags); + } + + @Override + CopyFlagSet getFlagSet(CopyFlags flag) { + return CopyFlagSet.of(flag); + } + + @Override + Class getFlagType() { + return CopyFlags.class; + } + + @Override + Function, CopyFlagSet> getConstructor() { + return CopyFlagSetImpl::new; + } + + /** + * {@link FlagSet#isSet(MaskedFlag)} on the flag enum is tested in {@link AbstractFlagSetTest} but + * the coverage check doesn't seem to notice it. + */ + @Test + void testIsSet() { + assertThat(CopyFlags.MDB_CP_COMPACT.isSet(CopyFlags.MDB_CP_COMPACT)).isTrue(); + //noinspection ConstantValue + assertThat(CopyFlags.MDB_CP_COMPACT.isSet(null)).isFalse(); + } +} diff --git a/src/test/java/org/lmdbjava/CursorDeprecatedTest.java b/src/test/java/org/lmdbjava/CursorDeprecatedTest.java new file mode 100644 index 00000000..a4528577 --- /dev/null +++ b/src/test/java/org/lmdbjava/CursorDeprecatedTest.java @@ -0,0 +1,350 @@ +/* + * 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.lang.Long.BYTES; +import static java.lang.Long.MIN_VALUE; +import static java.nio.ByteBuffer.allocateDirect; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.lmdbjava.ByteBufferProxy.PROXY_OPTIMAL; +import static org.lmdbjava.ByteUnit.MEBIBYTES; +import static org.lmdbjava.DbiFlags.MDB_CREATE; +import static org.lmdbjava.DbiFlags.MDB_DUPFIXED; +import static org.lmdbjava.DbiFlags.MDB_DUPSORT; +import static org.lmdbjava.Env.create; +import static org.lmdbjava.EnvFlags.MDB_NOSUBDIR; +import static org.lmdbjava.PutFlags.MDB_APPENDDUP; +import static org.lmdbjava.PutFlags.MDB_MULTIPLE; +import static org.lmdbjava.PutFlags.MDB_NODUPDATA; +import static org.lmdbjava.PutFlags.MDB_NOOVERWRITE; +import static org.lmdbjava.SeekOp.MDB_FIRST; +import static org.lmdbjava.SeekOp.MDB_GET_BOTH; +import static org.lmdbjava.TestUtils.DB_1; +import static org.lmdbjava.TestUtils.bb; + +import java.nio.ByteBuffer; +import java.nio.file.Path; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.lmdbjava.Txn.NotReadyException; +import org.lmdbjava.Txn.ReadOnlyRequiredException; + +/** + * Tests all the deprecated methods in {@link Cursor}. Essentially a duplicate of {@link + * CursorTest}. When all the deprecated methods are deleted we can delete this test class. + * + * @deprecated Tests all the deprecated methods in {@link Cursor}. + */ +@Deprecated +public class CursorDeprecatedTest { + + private TempDir tempDir; + private Env env; + + @BeforeEach + void beforeEach() { + tempDir = new TempDir(); + Path file = tempDir.createTempFile(); + env = + create(PROXY_OPTIMAL) + .setMapSize(MEBIBYTES.toBytes(1)) + .setMaxReaders(1) + .setMaxDbs(1) + .open(file.toFile(), Env.Builder.POSIX_MODE_DEFAULT, MDB_NOSUBDIR); + } + + @AfterEach + void afterEach() { + env.close(); + tempDir.cleanup(); + } + + @Test + void count() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + try (Txn txn = env.txnWrite(); + Cursor c = db.openCursor(txn)) { + c.put(bb(1), bb(2), new PutFlags[] {MDB_APPENDDUP}); + assertThat(c.count()).isEqualTo(1L); + c.put(bb(1), bb(4), new PutFlags[] {MDB_APPENDDUP}); + c.put(bb(1), bb(6), new PutFlags[] {MDB_APPENDDUP}); + assertThat(c.count()).isEqualTo(3L); + c.put(bb(2), bb(1), new PutFlags[] {MDB_APPENDDUP}); + c.put(bb(2), bb(2), new PutFlags[] {MDB_APPENDDUP}); + assertThat(c.count()).isEqualTo(2L); + } + } + + @Test + void cursorCannotCloseIfTransactionCommitted() { + assertThatThrownBy( + () -> { + final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + try (Txn txn = env.txnWrite()) { + try (Cursor c = db.openCursor(txn); ) { + c.put(bb(1), bb(2), new PutFlags[] {MDB_APPENDDUP}); + assertThat(c.count()).isEqualTo(1L); + c.put(bb(1), bb(4), new PutFlags[] {MDB_APPENDDUP}); + assertThat(c.count()).isEqualTo(2L); + txn.commit(); + } + } + }) + .isInstanceOf(NotReadyException.class); + } + + @Test + void cursorFirstLastNextPrev() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + try (Txn txn = env.txnWrite(); + Cursor c = db.openCursor(txn)) { + c.put(bb(1), bb(2), new PutFlags[] {MDB_NOOVERWRITE}); + c.put(bb(3), bb(4), new PutFlags[0]); + c.put(bb(5), bb(6), new PutFlags[0]); + c.put(bb(7), bb(8), new PutFlags[0]); + + assertThat(c.first()).isTrue(); + assertThat(c.key().getInt(0)).isEqualTo(1); + assertThat(c.val().getInt(0)).isEqualTo(2); + + assertThat(c.last()).isTrue(); + assertThat(c.key().getInt(0)).isEqualTo(7); + assertThat(c.val().getInt(0)).isEqualTo(8); + + assertThat(c.prev()).isTrue(); + assertThat(c.key().getInt(0)).isEqualTo(5); + assertThat(c.val().getInt(0)).isEqualTo(6); + + assertThat(c.first()).isTrue(); + assertThat(c.next()).isTrue(); + assertThat(c.key().getInt(0)).isEqualTo(3); + assertThat(c.val().getInt(0)).isEqualTo(4); + } + } + + @Test + void delete1() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + try (Txn txn = env.txnWrite(); + Cursor c = db.openCursor(txn)) { + c.put(bb(1), bb(2), MDB_NOOVERWRITE); + c.put(bb(3), bb(4), new PutFlags[0]); + assertThat(c.seek(MDB_FIRST)).isTrue(); + assertThat(c.key().getInt()).isEqualTo(1); + assertThat(c.val().getInt()).isEqualTo(2); + c.delete(new PutFlags[0]); + assertThat(c.seek(MDB_FIRST)).isTrue(); + assertThat(c.key().getInt()).isEqualTo(3); + assertThat(c.val().getInt()).isEqualTo(4); + c.delete(new PutFlags[0]); + assertThat(c.seek(MDB_FIRST)).isFalse(); + } + } + + @Test + void delete2() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + try (Txn txn = env.txnWrite(); + Cursor c = db.openCursor(txn)) { + c.put(bb(1), bb(2), MDB_NOOVERWRITE); + c.put(bb(3), bb(4), new PutFlags[0]); + assertThat(c.seek(MDB_FIRST)).isTrue(); + assertThat(c.key().getInt()).isEqualTo(1); + assertThat(c.val().getInt()).isEqualTo(2); + c.delete((PutFlags[]) null); + assertThat(c.seek(MDB_FIRST)).isTrue(); + assertThat(c.key().getInt()).isEqualTo(3); + assertThat(c.val().getInt()).isEqualTo(4); + c.delete((PutFlags[]) null); + assertThat(c.seek(MDB_FIRST)).isFalse(); + } + } + + @Test + void delete3() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + try (Txn txn = env.txnWrite(); + Cursor c = db.openCursor(txn)) { + c.put(bb(1), bb(2), MDB_NOOVERWRITE); + c.put(bb(3), bb(4), new PutFlags[0]); + assertThat(c.seek(MDB_FIRST)).isTrue(); + assertThat(c.key().getInt()).isEqualTo(1); + assertThat(c.val().getInt()).isEqualTo(2); + c.delete((PutFlags) null); + assertThat(c.seek(MDB_FIRST)).isTrue(); + assertThat(c.key().getInt()).isEqualTo(3); + assertThat(c.val().getInt()).isEqualTo(4); + c.delete((PutFlags) null); + assertThat(c.seek(MDB_FIRST)).isFalse(); + } + } + + @Test + void getKeyVal() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + try (Txn txn = env.txnWrite(); + Cursor c = db.openCursor(txn)) { + c.put(bb(1), bb(2), new PutFlags[] {MDB_APPENDDUP}); + c.put(bb(1), bb(4), new PutFlags[] {MDB_APPENDDUP}); + c.put(bb(1), bb(6), new PutFlags[] {MDB_APPENDDUP}); + c.put(bb(2), bb(1), new PutFlags[] {MDB_APPENDDUP}); + c.put(bb(2), bb(2), new PutFlags[] {MDB_APPENDDUP}); + c.put(bb(2), bb(3), new PutFlags[] {MDB_APPENDDUP}); + c.put(bb(2), bb(4), new PutFlags[] {MDB_APPENDDUP}); + assertThat(c.get(bb(1), bb(2), MDB_GET_BOTH)).isTrue(); + assertThat(c.count()).isEqualTo(3L); + assertThat(c.get(bb(1), bb(3), MDB_GET_BOTH)).isFalse(); + assertThat(c.get(bb(2), bb(1), MDB_GET_BOTH)).isTrue(); + assertThat(c.count()).isEqualTo(4L); + assertThat(c.get(bb(2), bb(0), MDB_GET_BOTH)).isFalse(); + } + } + + @Test + void putMultiple() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT, MDB_DUPFIXED); + final int elemCount = 20; + + final ByteBuffer values = allocateDirect(Integer.BYTES * elemCount); + for (int i = 1; i <= elemCount; i++) { + values.putInt(i); + } + values.flip(); + + final int key = 100; + final ByteBuffer k = bb(key); + try (Txn txn = env.txnWrite(); + Cursor c = db.openCursor(txn)) { + c.putMultiple(k, values, elemCount, new PutFlags[] {MDB_MULTIPLE}); + assertThat(c.count()).isEqualTo((long) elemCount); + } + } + + @Test + void putMultipleWithoutMdbMultipleFlag() { + assertThatThrownBy( + () -> { + final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + try (Txn txn = env.txnWrite(); + Cursor c = db.openCursor(txn)) { + c.putMultiple(bb(100), bb(1), 1, new PutFlags[0]); + } + }) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void renewTxRw() { + assertThatThrownBy( + () -> { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + try (Txn txn = env.txnWrite()) { + assertThat(txn.isReadOnly()).isFalse(); + + try (Cursor c = db.openCursor(txn)) { + c.renew(txn); + } + } + }) + .isInstanceOf(ReadOnlyRequiredException.class); + } + + @Test + void repeatedCloseCausesNotError() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + try (Txn txn = env.txnWrite()) { + final Cursor c = db.openCursor(txn); + c.close(); + c.close(); + } + } + + @Test + void reserve() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final ByteBuffer key = bb(5); + try (Txn txn = env.txnWrite()) { + assertThat(db.get(txn, key)).isNull(); + try (Cursor c = db.openCursor(txn)) { + final ByteBuffer val = c.reserve(key, BYTES * 2, new PutFlags[0]); + assertThat(db.get(txn, key)).isNotNull(); + val.putLong(MIN_VALUE).flip(); + } + txn.commit(); + } + try (Txn txn = env.txnWrite()) { + final ByteBuffer val = db.get(txn, key); + assertThat(val.capacity()).isEqualTo(BYTES * 2); + assertThat(val.getLong()).isEqualTo(MIN_VALUE); + } + } + + @Test + void returnValueForNoDupData() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + try (Txn txn = env.txnWrite(); + Cursor c = db.openCursor(txn)) { + // ok + assertThat(c.put(bb(5), bb(6), new PutFlags[] {MDB_NODUPDATA})).isTrue(); + assertThat(c.put(bb(5), bb(7), new PutFlags[] {MDB_NODUPDATA})).isTrue(); + assertThat(c.put(bb(5), bb(6), new PutFlags[] {MDB_NODUPDATA})).isFalse(); + } + } + + @Test + void returnValueForNoOverwrite() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + try (Txn txn = env.txnWrite(); + Cursor c = db.openCursor(txn)) { + // ok + assertThat(c.put(bb(5), bb(6), new PutFlags[] {MDB_NOOVERWRITE})).isTrue(); + // fails, but gets exist val + assertThat(c.put(bb(5), bb(8), new PutFlags[] {MDB_NOOVERWRITE})).isFalse(); + assertThat(c.val().getInt(0)).isEqualTo(6); + } + } + + @Test + void testCursorByteBufferDuplicate() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + try (Txn txn = env.txnWrite()) { + try (Cursor c = db.openCursor(txn)) { + c.put(bb(1), bb(2), new PutFlags[0]); + c.put(bb(3), bb(4), new PutFlags[0]); + } + txn.commit(); + } + try (Txn txn = env.txnRead()) { + try (Cursor c = db.openCursor(txn)) { + c.first(); + final ByteBuffer key1 = c.key().duplicate(); + final ByteBuffer val1 = c.val().duplicate(); + + c.last(); + final ByteBuffer key2 = c.key().duplicate(); + final ByteBuffer val2 = c.val().duplicate(); + + assertThat(key1.getInt(0)).isEqualTo(1); + assertThat(val1.getInt(0)).isEqualTo(2); + + assertThat(key2.getInt(0)).isEqualTo(3); + assertThat(val2.getInt(0)).isEqualTo(4); + } + } + } +} diff --git a/src/test/java/org/lmdbjava/CursorIterableIntegerKeyTest.java b/src/test/java/org/lmdbjava/CursorIterableIntegerKeyTest.java new file mode 100644 index 00000000..c562ed15 --- /dev/null +++ b/src/test/java/org/lmdbjava/CursorIterableIntegerKeyTest.java @@ -0,0 +1,661 @@ +/* + * 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.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.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 TempDir tempDir; + private Env env; + private Deque list; + + @Parameter public DbiFactory dbiFactory; + + @BeforeEach + public void before() throws IOException { + tempDir = new TempDir(); + final BufferProxy bufferProxy = ByteBufferProxy.PROXY_OPTIMAL; + env = + Env.create(bufferProxy) + .setMapSize(256, ByteUnit.KIBIBYTES) + .setMaxReaders(1) + .setMaxDbs(3) + .setEnvFlags(MDB_NOSUBDIR) + .open(tempDir.createTempFile()); + + populateTestDataList(); + } + + @AfterEach + public void after() { + env.close(); + tempDir.cleanup(); + } + + @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.createDbi() + .setDbName(DB_1) + .withIteratorComparator(ignored -> comparator) + .setDbiFlags(MDB_CREATE) + .open(); + 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.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(DBI_FLAGS) + .open()); + final DbiFactory nativeComparatorDb = + new DbiFactory( + "nativeComparator", + env -> + env.createDbi() + .setDbName(DB_2) + .withNativeComparator() + .setDbiFlags(DBI_FLAGS) + .open()); + final DbiFactory callbackComparatorDb = + new DbiFactory( + "callbackComparator", + env -> + env.createDbi() + .setDbName(DB_3) + .withCallbackComparator(MyArgumentProvider::buildComparator) + .setDbiFlags(DBI_FLAGS) + .open()); + final DbiFactory iteratorComparatorDb = + new DbiFactory( + "iteratorComparator", + env -> + env.createDbi() + .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..198ffd28 --- /dev/null +++ b/src/test/java/org/lmdbjava/CursorIterablePerfTest.java @@ -0,0 +1,193 @@ +/* + * 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.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.bb; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +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; + +class CursorIterablePerfTest { + + private static final int ITERATIONS = 100_000; + + private TempDir tempDir; + private final List> dbs = new ArrayList<>(); + private final List data = new ArrayList<>(ITERATIONS); + private Env env; + + @BeforeEach + public void before() { + tempDir = new TempDir(); + final BufferProxy bufferProxy = ByteBufferProxy.PROXY_OPTIMAL; + env = + create(bufferProxy) + .setMapSize(1, ByteUnit.GIBIBYTES) + .setMaxReaders(1) + .setMaxDbs(3) + .setEnvFlags(MDB_NOSUBDIR) + .open(tempDir.createTempFile()); + + final DbiFlagSet dbiFlagSet = MDB_CREATE; + // Use a java comparator for start/stop keys only + Dbi dbJavaComparator = + env.createDbi() + .setDbName("JavaComparator") + .withDefaultComparator() + .setDbiFlags(dbiFlagSet) + .open(); + // Use LMDB comparator for start/stop keys + Dbi dbLmdbComparator = + env.createDbi() + .setDbName("LmdbComparator") + .withNativeComparator() + .setDbiFlags(dbiFlagSet) + .open(); + + // Use a java comparator for start/stop keys and as a callback comparator + Dbi dbCallbackComparator = + env.createDbi() + .setDbName("CallBackComparator") + .withCallbackComparator(bufferProxy::getComparator) + .setDbiFlags(dbiFlagSet) + .open(); + + dbs.add(dbJavaComparator); + dbs.add(dbLmdbComparator); + dbs.add(dbCallbackComparator); + + populateList(); + } + + @AfterEach + public void after() { + env.close(); + tempDir.cleanup(); + } + + 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 = db.getNameAsString(); + + final Instant start = Instant.now(); + int cnt = 0; + // Exercise the stop key comparator on every entry + try (Txn txn = env.txnRead(); + CursorIterable cursorIterable = db.iterate(txn, keyRange)) { + for (final CursorIterable.KeyVal ignored : cursorIterable) { + 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/CursorIterableRangeTest.java b/src/test/java/org/lmdbjava/CursorIterableRangeTest.java new file mode 100644 index 00000000..ab76d3fc --- /dev/null +++ b/src/test/java/org/lmdbjava/CursorIterableRangeTest.java @@ -0,0 +1,424 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.lmdbjava; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.lmdbjava.Env.create; +import static org.lmdbjava.TestUtils.DB_1; +import static org.lmdbjava.TestUtils.bb; +import static org.lmdbjava.TestUtils.bbNative; +import static org.lmdbjava.TestUtils.parseInt; +import static org.lmdbjava.TestUtils.parseLong; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Function; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvFileSource; +import org.lmdbjava.CursorIterable.KeyVal; + +/** Test {@link CursorIterable}. */ +public final class CursorIterableRangeTest { + + private static final DbiFlagSet FLAGSET_DUPSORT = + DbiFlagSet.of(DbiFlags.MDB_CREATE, DbiFlags.MDB_DUPSORT); + private static final DbiFlagSet FLAGSET_INTEGERKEY = + DbiFlagSet.of(DbiFlags.MDB_CREATE, DbiFlags.MDB_INTEGERKEY); + private static final DbiFlagSet FLAGSET_INTEGERKEY_DUPSORT = + DbiFlagSet.of(DbiFlags.MDB_CREATE, DbiFlags.MDB_INTEGERKEY, DbiFlags.MDB_DUPSORT); + + @ParameterizedTest(name = "{index} => {0}: ({1}, {2})") + @CsvFileSource(resources = "/CursorIterableRangeTest/testSignedComparator.csv") + void testSignedComparator( + final String keyType, final String startKey, final String stopKey, final String expectedKV) { + testCSV( + builder -> builder.withCallbackComparator(ignored -> ByteBuffer::compareTo), + createBasicDBPopulator(), + DbiFlags.MDB_CREATE, + keyType, + startKey, + stopKey, + expectedKV); + } + + @ParameterizedTest(name = "{index} => {0}: ({1}, {2})") + @CsvFileSource(resources = "/CursorIterableRangeTest/testUnsignedComparator.csv") + void testUnsignedComparator( + final String keyType, final String startKey, final String stopKey, final String expectedKV) { + testCSV(createBasicDBPopulator(), DbiFlags.MDB_CREATE, keyType, startKey, stopKey, expectedKV); + } + + @ParameterizedTest(name = "{index} => {0}: ({1}, {2})") + @CsvFileSource(resources = "/CursorIterableRangeTest/testUnsignedComparator.csv") + void testUnsignedComparator_Iterator( + final String keyType, final String startKey, final String stopKey, final String expectedKV) { + testCSV(createBasicDBPopulator(), DbiFlags.MDB_CREATE, keyType, startKey, stopKey, expectedKV); + } + + @ParameterizedTest(name = "{index} => {0}: ({1}, {2})") + @CsvFileSource(resources = "/CursorIterableRangeTest/testUnsignedComparator.csv") + void testUnsignedComparator_Callback( + final String keyType, final String startKey, final String stopKey, final String expectedKV) { + testCSV(createBasicDBPopulator(), DbiFlags.MDB_CREATE, keyType, startKey, stopKey, expectedKV); + } + + @ParameterizedTest(name = "{index} => {0}: ({1}, {2})") + @CsvFileSource(resources = "/CursorIterableRangeTest/testSignedComparatorDupsort.csv") + void testSignedComparatorDupsort( + final String keyType, final String startKey, final String stopKey, final String expectedKV) { + testCSV( + builder -> builder.withCallbackComparator(ignored -> ByteBuffer::compareTo), + createMultiDBPopulator(2), + FLAGSET_DUPSORT, + keyType, + startKey, + stopKey, + expectedKV); + } + + @ParameterizedTest(name = "{index} => {0}: ({1}, {2})") + @CsvFileSource(resources = "/CursorIterableRangeTest/testUnsignedComparatorDupsort.csv") + void testUnsignedComparatorDupsort( + final String keyType, final String startKey, final String stopKey, final String expectedKV) { + testCSV(createMultiDBPopulator(2), FLAGSET_DUPSORT, keyType, startKey, stopKey, expectedKV); + } + + @ParameterizedTest(name = "{index} => {0}: ({1}, {2})") + @CsvFileSource(resources = "/CursorIterableRangeTest/testIntegerKey.csv") + void testIntegerKey( + final String keyType, final String startKey, final String stopKey, final String expectedKV) { + testCSV( + createIntegerDBPopulator(), + FLAGSET_INTEGERKEY, + keyType, + startKey, + stopKey, + expectedKV, + Integer.BYTES, + ByteOrder.nativeOrder()); + } + + @ParameterizedTest(name = "{index} => {0}: ({1}, {2})") + @CsvFileSource(resources = "/CursorIterableRangeTest/testUnsignedComparatorDupsort.csv") + void testIntegerKeyDupSort( + final String keyType, final String startKey, final String stopKey, final String expectedKV) { + testCSV( + createMultiIntegerDBPopulator(2), + FLAGSET_INTEGERKEY_DUPSORT, + keyType, + startKey, + stopKey, + expectedKV, + Integer.BYTES, + ByteOrder.nativeOrder()); + } + + @ParameterizedTest(name = "{index} => {0}: ({1}, {2})") + @CsvFileSource(resources = "/CursorIterableRangeTest/testLongKey.csv") + void testLongKey( + final String keyType, final String startKey, final String stopKey, final String expectedKV) { + testCSV( + createLongDBPopulator(), + FLAGSET_INTEGERKEY, + keyType, + startKey, + stopKey, + expectedKV, + Long.BYTES, + ByteOrder.nativeOrder()); + } + + @ParameterizedTest(name = "{index} => {0}: ({1}, {2})") + @CsvFileSource(resources = "/CursorIterableRangeTest/testUnsignedComparatorDupsort.csv") + void testLongKeyDupSort( + final String keyType, final String startKey, final String stopKey, final String expectedKV) { + testCSV( + createMultiLongDBPopulator(2), + FLAGSET_INTEGERKEY_DUPSORT, + keyType, + startKey, + stopKey, + expectedKV, + Long.BYTES, + ByteOrder.nativeOrder()); + } + + private void testCSV( + final Function, DbiBuilder.Stage3> comparatorFunc, + final BiConsumer, Dbi> dbPopulator, + final DbiFlagSet dbiFlags, + final String keyType, + final String startKey, + final String stopKey, + final String expectedKV) { + testCSV( + comparatorFunc, + dbPopulator, + dbiFlags, + keyType, + startKey, + stopKey, + expectedKV, + Integer.BYTES, + ByteOrder.BIG_ENDIAN); + } + + private void testCSV( + final BiConsumer, Dbi> dbPopulator, + final DbiFlagSet dbiFlags, + final String keyType, + final String startKey, + final String stopKey, + final String expectedKV) { + testCSV( + dbPopulator, + dbiFlags, + keyType, + startKey, + stopKey, + expectedKV, + Integer.BYTES, + ByteOrder.BIG_ENDIAN); + } + + private void testCSV( + final BiConsumer, Dbi> dbPopulator, + final DbiFlagSet dbiFlags, + final String keyType, + final String startKey, + final String stopKey, + final String expectedKV, + final int keyLen, + final ByteOrder byteOrder) { + + // We want to assert that the behaviour of all 4 comparator functions + // is identical. + + final List, DbiBuilder.Stage3>> + comparatorFuncs = new ArrayList<>(); + + // First test with our default iterator comparator + comparatorFuncs.add(DbiBuilder.Stage2::withDefaultComparator); + // Now test with mdp_cmp doing all comparisons, should be the same + comparatorFuncs.add(DbiBuilder.Stage2::withNativeComparator); + // Now test with the java callback comparator doing all the work + comparatorFuncs.add( + byteBufferStage2 -> + byteBufferStage2.withCallbackComparator(ByteBufferProxy.PROXY_OPTIMAL::getComparator)); + // Now test with the java comparator for iteration only + comparatorFuncs.add( + byteBufferStage2 -> + byteBufferStage2.withIteratorComparator(ByteBufferProxy.PROXY_OPTIMAL::getComparator)); + + for (Function, DbiBuilder.Stage3> comparatorFunc : + comparatorFuncs) { + testCSV( + comparatorFunc, + dbPopulator, + dbiFlags, + keyType, + startKey, + stopKey, + expectedKV, + keyLen, + byteOrder); + } + } + + private void testCSV( + final Function, DbiBuilder.Stage3> comparatorFunc, + final BiConsumer, Dbi> dbPopulator, + final DbiFlagSet dbiFlags, + final String keyType, + final String startKey, + final String stopKey, + final String expectedKV, + final int keyLen, + final ByteOrder byteOrder) { + try (final TempDir tempDir = new TempDir()) { + final Path file = tempDir.createTempFile(); + try (final Env env = + create() + .setMapSize(256, ByteUnit.KIBIBYTES) + .setMaxReaders(1) + .setMaxDbs(1) + .setEnvFlags(EnvFlags.MDB_NOSUBDIR) + .open(file)) { + + final DbiBuilder.Stage2 builderStage2 = env.createDbi().setDbName(DB_1); + final DbiBuilder.Stage3 builderStage3 = comparatorFunc.apply(builderStage2); + final Dbi dbi = builderStage3.setDbiFlags(dbiFlags).open(); + + dbPopulator.accept(env, dbi); + try (final Writer writer = new StringWriter()) { + final KeyRangeType keyRangeType = KeyRangeType.valueOf(keyType.trim()); + ByteBuffer start = parseKey(startKey, keyLen, byteOrder); + ByteBuffer stop = parseKey(stopKey, keyLen, byteOrder); + + final KeyRange keyRange = new KeyRange<>(keyRangeType, start, stop); + try (Txn txn = env.txnRead(); + CursorIterable c = dbi.iterate(txn, keyRange)) { + for (final KeyVal kv : c) { + final long key = getLong(kv.key(), byteOrder); + final long val = getLong(kv.val(), ByteOrder.BIG_ENDIAN); + writer.append("["); + writer.append(String.valueOf(key)); + writer.append(" "); + writer.append(String.valueOf(val)); + writer.append("]"); + } + } + assertThat(writer.toString()).isEqualTo(expectedKV == null ? "" : expectedKV); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + } + } + } + + private ByteBuffer parseKey(final String key, final int keyLen, final ByteOrder byteOrder) { + if (key != null) { + if (ByteOrder.nativeOrder().equals(byteOrder)) { + if (keyLen == Integer.BYTES) { + return bbNative(parseInt(key)); + } else { + return bbNative(parseLong(key)); + } + } else { + if (keyLen == Integer.BYTES) { + return bb(parseInt(key)); + } else { + return bb(parseLong(key)); + } + } + } + return null; + } + + private long getLong(final ByteBuffer byteBuffer, final ByteOrder byteOrder) { + byteBuffer.order(byteOrder); + if (byteBuffer.remaining() == Integer.BYTES) { + return byteBuffer.getInt(); + } else { + return byteBuffer.getLong(); + } + } + + private BiConsumer, Dbi> createBasicDBPopulator() { + return (env, dbi) -> { + try (Txn txn = env.txnWrite()) { + final Cursor c = dbi.openCursor(txn); + c.put(bb(0), bb(1)); + c.put(bb(2), bb(3)); + c.put(bb(4), bb(5)); + c.put(bb(6), bb(7)); + c.put(bb(8), bb(9)); + c.put(bb(-2), bb(-1)); + txn.commit(); + } + }; + } + + private BiConsumer, Dbi> createMultiDBPopulator(final int copies) { + return (env, dbi) -> { + try (Txn txn = env.txnWrite()) { + final Cursor c = dbi.openCursor(txn); + for (int i = 0; i < copies; i++) { + c.put(bb(0), bb(1 + i)); + c.put(bb(2), bb(3 + i)); + c.put(bb(4), bb(5 + i)); + c.put(bb(6), bb(7 + i)); + c.put(bb(8), bb(9 + i)); + c.put(bb(-2), bb(-1 + i)); + } + txn.commit(); + } + }; + } + + private BiConsumer, Dbi> createMultiIntegerDBPopulator( + final int copies) { + return (env, dbi) -> { + try (Txn txn = env.txnWrite()) { + final Cursor c = dbi.openCursor(txn); + for (int i = 0; i < copies; i++) { + c.put(bbNative(0), bb(1 + i)); + c.put(bbNative(2), bb(3 + i)); + c.put(bbNative(4), bb(5 + i)); + c.put(bbNative(6), bb(7 + i)); + c.put(bbNative(8), bb(9 + i)); + c.put(bbNative(-2), bb(-1 + i)); + } + txn.commit(); + } + }; + } + + private BiConsumer, Dbi> createMultiLongDBPopulator( + final int copies) { + return (env, dbi) -> { + try (Txn txn = env.txnWrite()) { + final Cursor c = dbi.openCursor(txn); + for (int i = 0; i < copies; i++) { + c.put(bbNative(0L), bb(1 + i)); + c.put(bbNative(2L), bb(3 + i)); + c.put(bbNative(4L), bb(5 + i)); + c.put(bbNative(6L), bb(7 + i)); + c.put(bbNative(8L), bb(9 + i)); + c.put(bbNative(-2L), bb(-1 + i)); + } + txn.commit(); + } + }; + } + + private BiConsumer, Dbi> createIntegerDBPopulator() { + return (env, dbi) -> { + try (Txn txn = env.txnWrite()) { + final Cursor c = dbi.openCursor(txn); + c.put(bbNative(0), bb(1)); + c.put(bbNative(1000), bb(2)); + c.put(bbNative(1000000), bb(3)); + c.put(bbNative(-1000000), bb(4)); + c.put(bbNative(-1000), bb(5)); + txn.commit(); + } + }; + } + + private BiConsumer, Dbi> createLongDBPopulator() { + return (env, dbi) -> { + try (Txn txn = env.txnWrite()) { + final Cursor c = dbi.openCursor(txn); + c.put(bbNative(0L), bb(1)); + c.put(bbNative(1000L), bb(2)); + c.put(bbNative(1000000L), bb(3)); + c.put(bbNative(-1000000L), bb(4)); + c.put(bbNative(-1000L), bb(5)); + txn.commit(); + } + }; + } +} diff --git a/src/test/java/org/lmdbjava/CursorIterableTest.java b/src/test/java/org/lmdbjava/CursorIterableTest.java index 22f5f7c7..d90c3c23 100644 --- a/src/test/java/org/lmdbjava/CursorIterableTest.java +++ b/src/test/java/org/lmdbjava/CursorIterableTest.java @@ -16,7 +16,6 @@ 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.assertj.core.api.Assertions.assertThatThrownBy; @@ -43,12 +42,13 @@ 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; import java.nio.ByteBuffer; -import java.nio.file.Path; import java.util.ArrayList; import java.util.Comparator; import java.util.Deque; @@ -56,35 +56,65 @@ 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}. */ +@ParameterizedClass(name = "{index}: dbi: {0}") +@ArgumentsSource(CursorIterableTest.MyArgumentProvider.class) public final class CursorIterableTest { - private Path file; - private Dbi db; + private static final DbiFlagSet DBI_FLAGS = MDB_CREATE; + private static final BufferProxy BUFFER_PROXY = ByteBufferProxy.PROXY_OPTIMAL; + + private TempDir tempDir; 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(); + tempDir = new TempDir(); + final BufferProxy bufferProxy = ByteBufferProxy.PROXY_OPTIMAL; env = - create() - .setMapSize(KIBIBYTES.toBytes(256)) + create(bufferProxy) + .setMapSize(256, ByteUnit.KIBIBYTES) .setMaxReaders(1) - .setMaxDbs(1) - .open(file.toFile(), POSIX_MODE, MDB_NOSUBDIR); - db = env.openDbi(DB_1, MDB_CREATE); - populateDatabase(db); + .setMaxDbs(3) + .setEnvFlags(MDB_NOSUBDIR) + .open(tempDir.createTempFile()); + + populateTestDataList(); } - private void populateDatabase(final Dbi dbi) { + @AfterEach + void afterEach() { + env.close(); + tempDir.cleanup(); + } + + 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 +125,6 @@ private void populateDatabase(final Dbi dbi) { } } - @AfterEach - void afterEach() { - env.close(); - FileUtil.delete(file); - } - @Test void allBackwardTest() { verify(allBackward(), 8, 6, 4, 2); @@ -180,6 +204,7 @@ void greaterThanTest() { void iterableOnlyReturnedOnce() { assertThatThrownBy( () -> { + final Dbi db = getDb(); try (Txn txn = env.txnRead(); CursorIterable c = db.iterate(txn)) { c.iterator(); // ok @@ -191,8 +216,10 @@ void iterableOnlyReturnedOnce() { @Test void iterate() { + final Dbi db = getDb(); try (Txn txn = env.txnRead(); CursorIterable c = db.iterate(txn)) { + for (final KeyVal kv : c) { assertThat(kv.key().getInt()).isEqualTo(list.pollFirst()); assertThat(kv.val().getInt()).isEqualTo(list.pollFirst()); @@ -204,6 +231,7 @@ void iterate() { void iteratorOnlyReturnedOnce() { assertThatThrownBy( () -> { + final Dbi db = getDb(); try (Txn txn = env.txnRead(); CursorIterable c = db.iterate(txn)) { c.iterator(); // ok @@ -229,6 +257,8 @@ void lessThanTest() { void nextThrowsNoSuchElementExceptionIfNoMoreElements() { assertThatThrownBy( () -> { + final Dbi db = getDb(); + populateTestDataList(); try (Txn txn = env.txnRead(); CursorIterable c = db.iterate(txn)) { final Iterator> i = c.iterator(); @@ -273,7 +303,12 @@ void openClosedBackwardTestWithGuava() { bb2.reset(); return guava.compare(array1, array2); }; - final Dbi guavaDbi = env.openDbi(DB_1, comparator, MDB_CREATE); + final Dbi guavaDbi = + env.createDbi() + .setDbName(DB_1) + .withCallbackComparator(ignored -> comparator) + .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 +328,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,13 +344,14 @@ void removeOddElements() { } txn.commit(); } - verify(all(), 4, 8); + verify(db, all(), 4, 8); } @Test void nextWithClosedEnvTest() { assertThatThrownBy( () -> { + final Dbi db = getDb(); try (Txn txn = env.txnRead()) { try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { final Iterator> c = ci.iterator(); @@ -331,6 +368,7 @@ void nextWithClosedEnvTest() { void removeWithClosedEnvTest() { assertThatThrownBy( () -> { + final Dbi db = getDb(); try (Txn txn = env.txnWrite()) { try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { final Iterator> c = ci.iterator(); @@ -350,6 +388,7 @@ void removeWithClosedEnvTest() { void hasNextWithClosedEnvTest() { assertThatThrownBy( () -> { + final Dbi db = getDb(); try (Txn txn = env.txnRead()) { try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { final Iterator> c = ci.iterator(); @@ -366,6 +405,7 @@ void hasNextWithClosedEnvTest() { void forEachRemainingWithClosedEnvTest() { assertThatThrownBy( () -> { + final Dbi db = getDb(); try (Txn txn = env.txnRead()) { try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { final Iterator> c = ci.iterator(); @@ -378,12 +418,67 @@ void forEachRemainingWithClosedEnvTest() { .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 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(); @@ -401,4 +496,72 @@ 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.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(DBI_FLAGS) + .open()); + final DbiFactory nativeComparatorDb = + new DbiFactory( + "nativeComparator", + env -> + env.createDbi() + .setDbName(DB_2) + .withNativeComparator() + .setDbiFlags(DBI_FLAGS) + .open()); + final DbiFactory callbackComparatorDb = + new DbiFactory( + "callbackComparator", + env -> + env.createDbi() + .setDbName(DB_3) + .withCallbackComparator(BUFFER_PROXY::getComparator) + .setDbiFlags(DBI_FLAGS) + .open()); + final DbiFactory iteratorComparatorDb = + new DbiFactory( + "iteratorComparator", + env -> + env.createDbi() + .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/CursorParamTest.java b/src/test/java/org/lmdbjava/CursorParamTest.java index 1a3604be..28f60419 100644 --- a/src/test/java/org/lmdbjava/CursorParamTest.java +++ b/src/test/java/org/lmdbjava/CursorParamTest.java @@ -16,7 +16,6 @@ package org.lmdbjava; -import static com.jakewharton.byteunits.BinaryByteUnit.MEBIBYTES; import static java.lang.Long.BYTES; import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; @@ -37,7 +36,6 @@ import static org.lmdbjava.SeekOp.MDB_NEXT; import static org.lmdbjava.SeekOp.MDB_PREV; import static org.lmdbjava.TestUtils.DB_1; -import static org.lmdbjava.TestUtils.POSIX_MODE; import static org.lmdbjava.TestUtils.bb; import static org.lmdbjava.TestUtils.mdb; import static org.lmdbjava.TestUtils.nb; @@ -48,25 +46,33 @@ import java.util.stream.Stream; import org.agrona.DirectBuffer; import org.agrona.MutableDirectBuffer; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.support.ParameterDeclarations; /** Test {@link Cursor} with different buffer implementations. */ public final class CursorParamTest { - static Stream data() { - return Stream.of( - Arguments.argumentSet("ByteBufferRunner(PROXY_OPTIMAL)", new ByteBufferRunner(PROXY_OPTIMAL)), - Arguments.argumentSet("ByteBufferRunner(PROXY_SAFE)", new ByteBufferRunner(PROXY_SAFE)), - Arguments.argumentSet("ByteArrayRunner(PROXY_BA)", new ByteArrayRunner(PROXY_BA)), - Arguments.argumentSet("DirectBufferRunner", new DirectBufferRunner()), - Arguments.argumentSet("NettyBufferRunner", new NettyBufferRunner())); + static class MyArgumentProvider implements ArgumentsProvider { + @Override + public Stream provideArguments( + ParameterDeclarations parameters, ExtensionContext context) { + return Stream.of( + Arguments.argumentSet( + "ByteBufferRunner(PROXY_OPTIMAL)", new ByteBufferRunner(PROXY_OPTIMAL)), + Arguments.argumentSet("ByteBufferRunner(PROXY_SAFE)", new ByteBufferRunner(PROXY_SAFE)), + Arguments.argumentSet("ByteArrayRunner(PROXY_BA)", new ByteArrayRunner(PROXY_BA)), + Arguments.argumentSet("DirectBufferRunner", new DirectBufferRunner()), + Arguments.argumentSet("NettyBufferRunner", new NettyBufferRunner())); + } } @ParameterizedTest - @MethodSource("data") + @ArgumentsSource(MyArgumentProvider.class) void execute(final BufferRunner runner, @TempDir final Path tmp) { runner.execute(tmp); } @@ -88,7 +94,12 @@ protected AbstractBufferRunner(final BufferProxy proxy) { public final void execute(final Path tmp) { try (Env env = env(tmp)) { assertThat(env.getDbiNames()).isEmpty(); - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE, MDB_DUPSORT) + .open(); assertThat(env.getDbiNames().get(0)).isEqualTo(DB_1.getBytes(UTF_8)); try (Txn txn = env.txnWrite(); Cursor c = db.openCursor(txn)) { @@ -159,10 +170,11 @@ public final void execute(final Path tmp) { private Env env(final Path tmp) { return create(proxy) - .setMapSize(MEBIBYTES.toBytes(1)) + .setMapSize(1, ByteUnit.MEBIBYTES) .setMaxReaders(1) .setMaxDbs(1) - .open(tmp.resolve("db").toFile(), POSIX_MODE, MDB_NOSUBDIR); + .setEnvFlags(MDB_NOSUBDIR) + .open(tmp.resolve("db")); } } diff --git a/src/test/java/org/lmdbjava/CursorTest.java b/src/test/java/org/lmdbjava/CursorTest.java index a0c78855..deb75622 100644 --- a/src/test/java/org/lmdbjava/CursorTest.java +++ b/src/test/java/org/lmdbjava/CursorTest.java @@ -16,7 +16,6 @@ package org.lmdbjava; -import static com.jakewharton.byteunits.BinaryByteUnit.MEBIBYTES; import static java.lang.Long.BYTES; import static java.lang.Long.MIN_VALUE; import static java.nio.ByteBuffer.allocateDirect; @@ -37,12 +36,12 @@ import static org.lmdbjava.SeekOp.MDB_LAST; import static org.lmdbjava.SeekOp.MDB_NEXT; import static org.lmdbjava.TestUtils.DB_1; -import static org.lmdbjava.TestUtils.POSIX_MODE; import static org.lmdbjava.TestUtils.bb; import java.nio.ByteBuffer; import java.nio.file.Path; import java.util.function.Consumer; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -54,31 +53,38 @@ /** Test {@link Cursor}. */ public final class CursorTest { - private Path file; private Env env; + private TempDir tempDir; @BeforeEach void beforeEach() { - file = FileUtil.createTempFile(); + tempDir = new TempDir(); + Path file = tempDir.createTempFile(); env = create(PROXY_OPTIMAL) - .setMapSize(MEBIBYTES.toBytes(1)) + .setMapSize(1, ByteUnit.MEBIBYTES) .setMaxReaders(1) .setMaxDbs(1) - .open(file.toFile(), POSIX_MODE, MDB_NOSUBDIR); + .setEnvFlags(MDB_NOSUBDIR) + .open(file); } @AfterEach void afterEach() { env.close(); - FileUtil.delete(file); + tempDir.cleanup(); } @Test void closedCursorRejectsSubsequentGets() { assertThatThrownBy( () -> { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); try (Txn txn = env.txnWrite()) { final Cursor c = db.openCursor(txn); c.close(); @@ -174,8 +180,13 @@ void closedEnvRejectsDeleteCall() { } @Test - void count() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + void countWithDupsort() { + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE, MDB_DUPSORT) + .open(); try (Txn txn = env.txnWrite(); Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_APPENDDUP); @@ -189,11 +200,40 @@ void count() { } } + @Test + void countWithoutDupsort() { + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + try (Txn txn = env.txnWrite(); + Cursor c = db.openCursor(txn)) { + assertThat(c.put(bb(1), bb(2), MDB_NOOVERWRITE)).isTrue(); + assertThat(c.put(bb(1), bb(4))).isTrue(); + assertThat(c.put(bb(1), bb(6), PutFlagSet.EMPTY)).isTrue(); + assertThat(c.put(bb(1), bb(8), MDB_NOOVERWRITE)).isFalse(); + assertThat(c.put(bb(2), bb(1), MDB_NOOVERWRITE)).isTrue(); + assertThat(c.put(bb(2), bb(2))).isTrue(); + Assertions.assertThatThrownBy( + () -> { + c.put(bb(2), bb(2), (PutFlagSet) null); + }) + .isInstanceOf(NullPointerException.class); + assertThat(c.put(bb(2), bb(2))).isTrue(); + + final Stat stat = db.stat(txn); + assertThat(stat.entries).isEqualTo(2); + } + } + @Test void cursorCannotCloseIfTransactionCommitted() { assertThatThrownBy( () -> { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE, MDB_DUPSORT) + .open(); try (Txn txn = env.txnWrite()) { try (Cursor c = db.openCursor(txn); ) { c.put(bb(1), bb(2), MDB_APPENDDUP); @@ -209,7 +249,8 @@ void cursorCannotCloseIfTransactionCommitted() { @Test void cursorFirstLastNextPrev() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Txn txn = env.txnWrite(); Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_NOOVERWRITE); @@ -238,7 +279,12 @@ void cursorFirstLastNextPrev() { @Test void delete() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE, MDB_DUPSORT) + .open(); try (Txn txn = env.txnWrite(); Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_NOOVERWRITE); @@ -255,9 +301,62 @@ void delete() { } } + @Test + void delete2() { + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE, MDB_DUPSORT) + .open(); + try (Txn txn = env.txnWrite(); + Cursor c = db.openCursor(txn)) { + c.put(bb(1), bb(2), MDB_NOOVERWRITE); + c.put(bb(3), bb(4)); + assertThat(c.seek(MDB_FIRST)).isTrue(); + assertThat(c.key().getInt()).isEqualTo(1); + assertThat(c.val().getInt()).isEqualTo(2); + c.delete(PutFlags.EMPTY); + assertThat(c.seek(MDB_FIRST)).isTrue(); + assertThat(c.key().getInt()).isEqualTo(3); + assertThat(c.val().getInt()).isEqualTo(4); + c.delete(PutFlags.EMPTY); + assertThat(c.seek(MDB_FIRST)).isFalse(); + } + } + + @Test + void delete3() { + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE, MDB_DUPSORT) + .open(); + try (Txn txn = env.txnWrite(); + Cursor c = db.openCursor(txn)) { + c.put(bb(1), bb(2), MDB_NOOVERWRITE); + c.put(bb(3), bb(4)); + assertThat(c.seek(MDB_FIRST)).isTrue(); + assertThat(c.key().getInt()).isEqualTo(1); + assertThat(c.val().getInt()).isEqualTo(2); + c.delete((PutFlagSet) null); + assertThat(c.seek(MDB_FIRST)).isTrue(); + assertThat(c.key().getInt()).isEqualTo(3); + assertThat(c.val().getInt()).isEqualTo(4); + c.delete((PutFlagSet) null); + assertThat(c.seek(MDB_FIRST)).isFalse(); + } + } + @Test void getKeyVal() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE, MDB_DUPSORT) + .open(); try (Txn txn = env.txnWrite(); Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_APPENDDUP); @@ -278,7 +377,12 @@ void getKeyVal() { @Test void putMultiple() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT, MDB_DUPFIXED); + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE, MDB_DUPSORT, MDB_DUPFIXED) + .open(); final int elemCount = 20; final ByteBuffer values = allocateDirect(Integer.BYTES * elemCount); @@ -298,20 +402,62 @@ void putMultiple() { @Test void putMultipleWithoutMdbMultipleFlag() { - assertThatThrownBy( - () -> { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); - try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE, MDB_DUPSORT) + .open(); + try (Txn txn = env.txnWrite(); + Cursor c = db.openCursor(txn)) { + assertThatThrownBy( + () -> { c.putMultiple(bb(100), bb(1), 1); - } - }) - .isInstanceOf(IllegalArgumentException.class); + }) + .isInstanceOf(IllegalArgumentException.class); + } + } + + @Test + void putMultipleWithoutMdbMultipleFlag2() { + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE, MDB_DUPSORT) + .open(); + try (Txn txn = env.txnWrite(); + Cursor c = db.openCursor(txn)) { + assertThatThrownBy( + () -> { + c.putMultiple(bb(100), bb(1), 1, PutFlags.EMPTY); + }) + .isInstanceOf(IllegalArgumentException.class); + } + } + + @Test + void putMultipleWithoutMdbMultipleFlag3() { + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE, MDB_DUPSORT) + .open(); + try (Txn txn = env.txnWrite(); + Cursor c = db.openCursor(txn)) { + assertThatThrownBy( + () -> { + c.putMultiple(bb(100), bb(1), 1, (PutFlagSet) null); + }) + .isInstanceOf(NullPointerException.class); + } } @Test void renewTxRo() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); final Cursor c; try (Txn txn = env.txnRead()) { @@ -331,7 +477,12 @@ void renewTxRo() { void renewTxRw() { assertThatThrownBy( () -> { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); try (Txn txn = env.txnWrite()) { assertThat(txn.isReadOnly()).isFalse(); @@ -345,7 +496,12 @@ void renewTxRw() { @Test void repeatedCloseCausesNotError() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE, MDB_DUPSORT) + .open(); try (Txn txn = env.txnWrite()) { final Cursor c = db.openCursor(txn); c.close(); @@ -355,7 +511,8 @@ void repeatedCloseCausesNotError() { @Test void reserve() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); final ByteBuffer key = bb(5); try (Txn txn = env.txnWrite()) { assertThat(db.get(txn, key)).isNull(); @@ -375,7 +532,12 @@ void reserve() { @Test void returnValueForNoDupData() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE, MDB_DUPSORT) + .open(); try (Txn txn = env.txnWrite(); Cursor c = db.openCursor(txn)) { // ok @@ -387,7 +549,8 @@ void returnValueForNoDupData() { @Test void returnValueForNoOverwrite() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Txn txn = env.txnWrite(); Cursor c = db.openCursor(txn)) { // ok @@ -400,7 +563,8 @@ void returnValueForNoOverwrite() { @Test void testCursorByteBufferDuplicate() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Txn txn = env.txnWrite()) { try (Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2)); @@ -430,7 +594,8 @@ void testCursorByteBufferDuplicate() { private void doEnvClosedTest( final Consumer> workBeforeEnvClosed, final Consumer> workAfterEnvClose) { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); db.put(bb(1), bb(10)); db.put(bb(2), bb(20)); diff --git a/src/test/java/org/lmdbjava/DbiBuilderTest.java b/src/test/java/org/lmdbjava/DbiBuilderTest.java new file mode 100644 index 00000000..c06c3dc9 --- /dev/null +++ b/src/test/java/org/lmdbjava/DbiBuilderTest.java @@ -0,0 +1,206 @@ +/* + * 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 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.util.ArrayList; +import java.util.Collections; +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; + +class DbiBuilderTest { + + private TempDir tempDir; + private Env env; + + @BeforeEach + public void before() { + tempDir = new TempDir(); + env = + create() + .setMapSize(64, ByteUnit.MEBIBYTES) + .setMaxReaders(2) + .setMaxDbs(2) + .setEnvFlags(MDB_NOSUBDIR) + .open(tempDir.createTempFile()); + } + + @AfterEach + public void after() { + env.close(); + tempDir.cleanup(); + } + + @Test + public void unnamed() { + final Dbi dbi = + env.createDbi() + .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.createDbi() + .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.createDbi() + .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.createDbi() + .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.createDbi() + .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.createDbi() + .setDbName("foo") + .withDefaultComparator() + .setDbiFlags(DbiFlags.MDB_DUPSORT, DbiFlags.MDB_DUPFIXED) // Will get overwritten + .setDbiFlags() // clear them + .setDbiFlags(Collections.singletonList(DbiFlags.MDB_INTEGERDUP)) + .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/DbiDeprecatedTest.java b/src/test/java/org/lmdbjava/DbiDeprecatedTest.java new file mode 100644 index 00000000..7156b963 --- /dev/null +++ b/src/test/java/org/lmdbjava/DbiDeprecatedTest.java @@ -0,0 +1,675 @@ +/* + * 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.lang.Long.MAX_VALUE; +import static java.lang.System.getProperty; +import static java.nio.ByteBuffer.allocateDirect; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.nCopies; +import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.stream.Collectors.toList; +import static java.util.stream.IntStream.range; +import static org.assertj.core.api.Assertions.assertThat; +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.ByteUnit.MEBIBYTES; +import static org.lmdbjava.DbiFlags.*; +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 java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.*; +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.ToIntFunction; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.lmdbjava.CursorIterable.KeyVal; +import org.lmdbjava.Dbi.DbFullException; +import org.lmdbjava.Env.AlreadyClosedException; +import org.lmdbjava.Env.MapFullException; +import org.lmdbjava.LmdbNativeException.ConstantDerivedException; + +/** + * Tests all the deprecated methods in {@link Dbi}. Essentially a duplicate of {@link DbiTest}. When + * all the deprecated methods are deleted we can delete this test class. + * + * @deprecated Tests all the deprecated methods in {@link Dbi}. + */ +@Deprecated +public class DbiDeprecatedTest { + + private TempDir tempDir; + private Env env; + private TempDir tempDirBa; + private Env envBa; + + @BeforeEach + void beforeEach() { + tempDir = new TempDir(); + final Path file = tempDir.createTempFile(); + env = + create() + .setMapSize(MEBIBYTES.toBytes(64)) + .setMaxReaders(2) + .setMaxDbs(2) + .open(file.toFile(), MDB_NOSUBDIR); + tempDirBa = new TempDir(); + final Path fileBa = tempDirBa.createTempFile(); + envBa = + create(PROXY_BA) + .setMapSize(MEBIBYTES.toBytes(64)) + .setMaxReaders(2) + .setMaxDbs(2) + .open(fileBa.toFile(), MDB_NOSUBDIR); + } + + @AfterEach + void afterEach() { + env.close(); + envBa.close(); + tempDir.cleanup(); + tempDirBa.cleanup(); + } + + @Test + void close() { + assertThatThrownBy( + () -> { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + db.put(bb(1), bb(42)); + db.close(); + db.put(bb(2), bb(42)); // error + }) + .isInstanceOf(ConstantDerivedException.class); + } + + @Test + void customComparator() { + final Comparator reverseOrder = + (o1, o2) -> { + final int lexical = PROXY_OPTIMAL.getComparator().compare(o1, o2); + if (lexical == 0) { + return 0; + } + return lexical * -1; + }; + doCustomComparator(env, reverseOrder, TestUtils::bb, ByteBuffer::getInt); + } + + @Test + void customComparatorByteArray() { + final Comparator reverseOrder = + (o1, o2) -> { + final int lexical = PROXY_BA.getComparator().compare(o1, o2); + if (lexical == 0) { + return 0; + } + return lexical * -1; + }; + doCustomComparator(envBa, reverseOrder, TestUtils::ba, TestUtils::fromBa); + } + + private void doCustomComparator( + Env env, + Comparator comparator, + IntFunction serializer, + ToIntFunction deserializer) { + final Dbi db = env.openDbi(DB_1, comparator, true, MDB_CREATE); + try (Txn txn = env.txnWrite()) { + assertThat(db.put(txn, serializer.apply(2), serializer.apply(3))).isTrue(); + assertThat(db.put(txn, serializer.apply(4), serializer.apply(6))).isTrue(); + assertThat(db.put(txn, serializer.apply(6), serializer.apply(7))).isTrue(); + assertThat(db.put(txn, serializer.apply(8), serializer.apply(7))).isTrue(); + txn.commit(); + } + try (Txn txn = env.txnRead(); + 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); + assertThat(deserializer.applyAsInt(iter.next().key())).isEqualTo(4); + } + } + + @Test + void dbOpenMaxDatabases() { + assertThatThrownBy( + () -> { + env.openDbi("db1 is OK", MDB_CREATE); + env.openDbi("db2 is OK", MDB_CREATE); + env.openDbi("db3 fails", MDB_CREATE); + }) + .isInstanceOf(DbFullException.class); + } + + @Test + void dbiWithComparatorThreadSafety() { + doDbiWithComparatorThreadSafety( + env, PROXY_OPTIMAL::getComparator, TestUtils::bb, ByteBuffer::getInt); + } + + @Test + void dbiWithComparatorThreadSafetyByteArray() { + doDbiWithComparatorThreadSafety( + envBa, PROXY_BA::getComparator, TestUtils::ba, TestUtils::fromBa); + } + + private void doDbiWithComparatorThreadSafety( + Env env, + Function> comparator, + IntFunction serializer, + ToIntFunction deserializer) { + final DbiFlags[] flags = new DbiFlags[] {MDB_CREATE, MDB_INTEGERKEY}; + final Comparator c = comparator.apply(DbiFlagSet.of(flags)); + final Dbi db = env.openDbi(DB_1, c, true, flags); + + final List keys = range(0, 1_000).boxed().collect(toList()); + + final ExecutorService pool = Executors.newCachedThreadPool(); + final AtomicBoolean proceed = new AtomicBoolean(true); + final Future reader = + pool.submit( + () -> { + while (proceed.get()) { + try (Txn txn = env.txnRead()) { + db.get(txn, serializer.apply(50)); + } + } + }); + + for (final Integer key : keys) { + try (Txn txn = env.txnWrite()) { + db.put(txn, serializer.apply(key), serializer.apply(3)); + txn.commit(); + } + } + + try (Txn txn = env.txnRead(); + CursorIterable ci = db.iterate(txn)) { + final Iterator> iter = ci.iterator(); + final List result = new ArrayList<>(); + while (iter.hasNext()) { + result.add(deserializer.applyAsInt(iter.next().key())); + } + + assertThat(result).contains(keys.toArray(new Integer[0])); + } + + proceed.set(false); + try { + reader.get(1, SECONDS); + pool.shutdown(); + pool.awaitTermination(1, SECONDS); + } catch (ExecutionException | InterruptedException | TimeoutException e) { + throw new IllegalStateException(e); + } + } + + @Test + void drop() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + try (Txn txn = env.txnWrite()) { + db.put(txn, bb(1), bb(42)); + db.put(txn, bb(2), bb(42)); + assertThat(db.get(txn, bb(1))).isNotNull(); + assertThat(db.get(txn, bb(2))).isNotNull(); + db.drop(txn); + assertThat(db.get(txn, bb(1))).isNull(); // data gone + assertThat(db.get(txn, bb(2))).isNull(); + db.put(txn, bb(1), bb(42)); // ensure DB still works + db.put(txn, bb(2), bb(42)); + assertThat(db.get(txn, bb(1))).isNotNull(); + assertThat(db.get(txn, bb(2))).isNotNull(); + } + } + + @Test + void dropAndDelete() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi nameDb = env.openDbi((byte[]) null); + final byte[] dbNameBytes = DB_1.getBytes(UTF_8); + final ByteBuffer dbNameBuffer = allocateDirect(dbNameBytes.length); + dbNameBuffer.put(dbNameBytes).flip(); + + try (Txn txn = env.txnWrite()) { + assertThat(nameDb.get(txn, dbNameBuffer)).isNotNull(); + db.drop(txn, true); + assertThat(nameDb.get(txn, dbNameBuffer)).isNull(); + txn.commit(); + } + } + + @Test + void dropAndDeleteAnonymousDb() { + env.openDbi(DB_1, MDB_CREATE); + final Dbi nameDb = env.openDbi((byte[]) null); + final byte[] dbNameBytes = DB_1.getBytes(UTF_8); + final ByteBuffer dbNameBuffer = allocateDirect(dbNameBytes.length); + dbNameBuffer.put(dbNameBytes).flip(); + + try (Txn txn = env.txnWrite()) { + assertThat(nameDb.get(txn, dbNameBuffer)).isNotNull(); + nameDb.drop(txn, true); + assertThat(nameDb.get(txn, dbNameBuffer)).isNull(); + txn.commit(); + } + + nameDb.close(); // explicit close after drop is OK + } + + @Test + void getName() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + assertThat(db.getName()).isEqualTo(DB_1.getBytes(UTF_8)); + } + + @Test + void getNamesWhenDbisPresent() { + 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(); + assertThat(dbiNames).hasSize(2); + assertThat(dbiNames.get(0)).isEqualTo(dbHello); + assertThat(dbiNames.get(1)).isEqualTo(dbWorld); + } + + @Test + void getNamesWhenEmpty() { + final List dbiNames = env.getDbiNames(); + assertThat(dbiNames).isEmpty(); + } + + @Test + void listsFlags() { + final Dbi dbi = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT, MDB_REVERSEKEY); + + try (Txn txn = env.txnRead()) { + final List flags = dbi.listFlags(txn); + assertThat(flags).containsExactlyInAnyOrder(MDB_DUPSORT, MDB_REVERSEKEY); + } + } + + @Test + void putAbortGet() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + + try (Txn txn = env.txnWrite()) { + db.put(txn, bb(5), bb(5)); + txn.abort(); + } + + try (Txn txn = env.txnWrite()) { + assertThat(db.get(txn, bb(5))).isNull(); + } + } + + @Test + void putAndGetAndDeleteWithInternalTx() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + + db.put(bb(5), bb(5)); + try (Txn txn = env.txnRead()) { + final ByteBuffer found = db.get(txn, bb(5)); + assertThat(found).isNotNull(); + assertThat(txn.val().getInt()).isEqualTo(5); + } + assertThat(db.delete(bb(5))).isTrue(); + assertThat(db.delete(bb(5))).isFalse(); + + try (Txn txn = env.txnRead()) { + assertThat(db.get(txn, bb(5))).isNull(); + } + } + + @Test + void putCommitGet() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + try (Txn txn = env.txnWrite()) { + db.put(txn, bb(5), bb(5)); + txn.commit(); + } + + try (Txn txn = env.txnWrite()) { + final ByteBuffer found = db.get(txn, bb(5)); + assertThat(found).isNotNull(); + assertThat(txn.val().getInt()).isEqualTo(5); + } + } + + @Test + void putCommitGetByteArray() { + final Path file = tempDir.createTempFile(); + 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()) { + db.put(txn, ba(5), ba(5)); + txn.commit(); + } + try (Txn txn = envBa.txnWrite()) { + final byte[] found = db.get(txn, ba(5)); + assertThat(found).isNotNull(); + assertThat(fromBa(txn.val())).isEqualTo(5); + } + } + } + + @Test + void putDelete() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + + try (Txn txn = env.txnWrite()) { + db.put(txn, bb(5), bb(5)); + assertThat(db.delete(txn, bb(5))).isTrue(); + + assertThat(db.get(txn, bb(5))).isNull(); + txn.abort(); + } + } + + @Test + void putDuplicateDelete() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + + try (Txn txn = env.txnWrite()) { + db.put(txn, bb(5), bb(5)); + db.put(txn, bb(5), bb(6)); + db.put(txn, bb(5), bb(7)); + assertThat(db.delete(txn, bb(5), bb(6))).isTrue(); + assertThat(db.delete(txn, bb(5), bb(6))).isFalse(); + assertThat(db.delete(txn, bb(5), bb(5))).isTrue(); + assertThat(db.delete(txn, bb(5), bb(5))).isFalse(); + + try (Cursor cursor = db.openCursor(txn)) { + final ByteBuffer key = bb(5); + cursor.get(key, MDB_SET_KEY); + assertThat(cursor.count()).isEqualTo(1L); + } + txn.abort(); + } + } + + @Test + void putReserve() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + + final ByteBuffer key = bb(5); + try (Txn txn = env.txnWrite()) { + assertThat(db.get(txn, key)).isNull(); + final ByteBuffer val = db.reserve(txn, key, 32, MDB_NOOVERWRITE); + val.putLong(MAX_VALUE); + assertThat(db.get(txn, key)).isNotNull(); + txn.commit(); + } + try (Txn txn = env.txnWrite()) { + final ByteBuffer val = db.get(txn, key); + assertThat(val).isNotNull(); + assertThat(val.capacity()).isEqualTo(32); + assertThat(val.getLong()).isEqualTo(MAX_VALUE); + assertThat(val.getLong(8)).isEqualTo(0L); + } + } + + @Test + void putZeroByteValueForNonMdbDupSortDatabase() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + try (Txn txn = env.txnWrite()) { + final ByteBuffer val = allocateDirect(0); + db.put(txn, bb(5), val); + txn.commit(); + } + + try (Txn txn = env.txnRead()) { + final ByteBuffer found = db.get(txn, bb(5)); + assertThat(found).isNotNull(); + assertThat(txn.val().capacity()).isEqualTo(0); + } + } + + @Test + void returnValueForNoDupData() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + try (Txn txn = env.txnWrite()) { + // ok + assertThat(db.put(txn, bb(5), bb(6), MDB_NODUPDATA)).isTrue(); + assertThat(db.put(txn, bb(5), bb(7), MDB_NODUPDATA)).isTrue(); + assertThat(db.put(txn, bb(5), bb(6), MDB_NODUPDATA)).isFalse(); + } + } + + @Test + void returnValueForNoOverwrite() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + try (Txn txn = env.txnWrite()) { + // ok + assertThat(db.put(txn, bb(5), bb(6), MDB_NOOVERWRITE)).isTrue(); + // fails, but gets exist val + assertThat(db.put(txn, bb(5), bb(8), MDB_NOOVERWRITE)).isFalse(); + assertThat(txn.val().getInt(0)).isEqualTo(6); + } + } + + @Test + void stats() { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + db.put(bb(1), bb(42)); + db.put(bb(2), bb(42)); + db.put(bb(3), bb(42)); + final Stat stat; + try (Txn txn = env.txnRead()) { + stat = db.stat(txn); + } + assertThat(stat).isNotNull(); + assertThat(stat.branchPages).isEqualTo(0L); + assertThat(stat.depth).isEqualTo(1); + assertThat(stat.entries).isEqualTo(3L); + assertThat(stat.leafPages).isEqualTo(1L); + assertThat(stat.overflowPages).isEqualTo(0L); + assertThat(stat.pageSize % 4_096).isEqualTo(0); + } + + @Test + void testMapFullException() { + assertThatThrownBy( + () -> { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + try (Txn txn = env.txnWrite()) { + final ByteBuffer v; + try { + v = allocateDirect(1_024 * 1_024 * 1_024); + } catch (final OutOfMemoryError e) { + // Travis CI OS X build cannot allocate this much memory, so assume OK + throw new MapFullException(); + } + db.put(txn, bb(1), v); + } + }) + .isInstanceOf(MapFullException.class); + } + + @Test + void testParallelWritesStress() { + if (getProperty("os.name").startsWith("Windows")) { + return; // Windows VMs run this test too slowly + } + + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + + // Travis CI has 1.5 cores for legacy builds + nCopies(2, null).parallelStream() + .forEach( + ignored -> { + for (int i = 0; i < 15_000; i++) { + db.put(bb(i), bb(i)); + } + }); + } + + @Test + void closedEnvRejectsOpenCall() { + assertThatThrownBy( + () -> { + env.close(); + env.openDbi(DB_1, MDB_CREATE); + }) + .isInstanceOf(AlreadyClosedException.class); + } + + @Test + void closedEnvRejectsCloseCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, (db, txn) -> db.close()); + }) + .isInstanceOf(AlreadyClosedException.class); + } + + @Test + void closedEnvRejectsGetCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest( + (db, txn) -> { + final ByteBuffer valBuf = db.get(txn, bb(1)); + assertThat(valBuf.getInt()).isEqualTo(10); + }, + (db, txn) -> db.get(txn, bb(2))); + }) + .isInstanceOf(AlreadyClosedException.class); + } + + @Test + void closedEnvRejectsPutCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, (db, txn) -> db.put(bb(5), bb(50))); + }) + .isInstanceOf(AlreadyClosedException.class); + } + + @Test + void closedEnvRejectsPutWithTxnCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest( + null, + (db, txn) -> { + db.put(txn, bb(5), bb(50)); + }); + }) + .isInstanceOf(AlreadyClosedException.class); + } + + @Test + void closedEnvRejectsIterateCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, Dbi::iterate); + }) + .isInstanceOf(AlreadyClosedException.class); + } + + @Test + void closedEnvRejectsDropCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, Dbi::drop); + }) + .isInstanceOf(AlreadyClosedException.class); + } + + @Test + void closedEnvRejectsDropAndDeleteCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, (db, txn) -> db.drop(txn, true)); + }) + .isInstanceOf(AlreadyClosedException.class); + } + + @Test + void closedEnvRejectsOpenCursorCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, Dbi::openCursor); + }) + .isInstanceOf(AlreadyClosedException.class); + } + + @Test + void closedEnvRejectsReserveCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, (db, txn) -> db.reserve(txn, bb(1), 32, MDB_NOOVERWRITE)); + }) + .isInstanceOf(AlreadyClosedException.class); + } + + @Test + void closedEnvRejectsStatCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, Dbi::stat); + }) + .isInstanceOf(AlreadyClosedException.class); + } + + private void doEnvClosedTest( + final BiConsumer, Txn> workBeforeEnvClosed, + final BiConsumer, Txn> workAfterEnvClose) { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + + db.put(bb(1), bb(10)); + db.put(bb(2), bb(20)); + db.put(bb(2), bb(30)); + db.put(bb(4), bb(40)); + + try (Txn txn = env.txnWrite()) { + + if (workBeforeEnvClosed != null) { + workBeforeEnvClosed.accept(db, txn); + } + + env.close(); + + if (workAfterEnvClose != null) { + workAfterEnvClose.accept(db, txn); + } + } + } +} diff --git a/src/test/java/org/lmdbjava/DbiFlagSetTest.java b/src/test/java/org/lmdbjava/DbiFlagSetTest.java new file mode 100644 index 00000000..460ca0f2 --- /dev/null +++ b/src/test/java/org/lmdbjava/DbiFlagSetTest.java @@ -0,0 +1,89 @@ +/* + * 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.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class DbiFlagSetTest extends AbstractFlagSetTest { + + @Test + void test() { + // This is here purely to stop CodeQL moaning that this class is unused. + // All the actual tests are in the superclass + Assertions.assertThat(getAllFlags()).isNotNull(); + } + + @Override + List getAllFlags() { + return Arrays.stream(DbiFlags.values()).collect(Collectors.toList()); + } + + @Override + DbiFlagSet getEmptyFlagSet() { + return DbiFlagSet.empty(); + } + + @Override + AbstractFlagSet.Builder getBuilder() { + return DbiFlagSet.builder(); + } + + @Override + Class getFlagType() { + return DbiFlags.class; + } + + @Override + DbiFlagSet getFlagSet(Collection flags) { + return DbiFlagSet.of(flags); + } + + @Override + DbiFlagSet getFlagSet(DbiFlags[] flags) { + return DbiFlagSet.of(flags); + } + + @Override + DbiFlagSet getFlagSet(DbiFlags flag) { + return DbiFlagSet.of(flag); + } + + @Override + Function, DbiFlagSet> getConstructor() { + return DbiFlagSetImpl::new; + } + + /** + * {@link FlagSet#isSet(MaskedFlag)} on the flag enum is tested in {@link AbstractFlagSetTest} but + * the coverage check doesn't seem to notice it. + */ + @Test + void testIsSet() { + assertThat(DbiFlags.MDB_CREATE.isSet(DbiFlags.MDB_CREATE)).isTrue(); + assertThat(DbiFlags.MDB_CREATE.isSet(DbiFlags.MDB_REVERSEKEY)).isFalse(); + //noinspection ConstantValue + assertThat(DbiFlags.MDB_CREATE.isSet(null)).isFalse(); + } +} diff --git a/src/test/java/org/lmdbjava/DbiTest.java b/src/test/java/org/lmdbjava/DbiTest.java index 23b6790e..575937f5 100644 --- a/src/test/java/org/lmdbjava/DbiTest.java +++ b/src/test/java/org/lmdbjava/DbiTest.java @@ -16,7 +16,6 @@ package org.lmdbjava; -import static com.jakewharton.byteunits.BinaryByteUnit.MEBIBYTES; import static java.lang.Long.MAX_VALUE; import static java.lang.System.getProperty; import static java.nio.ByteBuffer.allocateDirect; @@ -29,14 +28,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 +49,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; @@ -62,42 +71,48 @@ /** Test {@link Dbi}. */ public final class DbiTest { - private Path file; + private TempDir tempDir; private Env env; - private Path fileBa; private Env envBa; @BeforeEach void beforeEach() { - file = FileUtil.createTempFile(); + tempDir = new TempDir(); + final Path file = tempDir.createTempFile(); env = create() - .setMapSize(MEBIBYTES.toBytes(64)) + .setMapSize(64, ByteUnit.MEBIBYTES) .setMaxReaders(2) .setMaxDbs(2) - .open(file.toFile(), MDB_NOSUBDIR); - fileBa = FileUtil.createTempFile(); + .setEnvFlags(MDB_NOSUBDIR) + .open(file); + final Path fileBa = tempDir.createTempFile(); envBa = create(PROXY_BA) - .setMapSize(MEBIBYTES.toBytes(64)) + .setMapSize(64, ByteUnit.MEBIBYTES) .setMaxReaders(2) .setMaxDbs(2) - .open(fileBa.toFile(), MDB_NOSUBDIR); + .setEnvFlags(MDB_NOSUBDIR) + .open(fileBa); } @AfterEach void afterEach() { env.close(); envBa.close(); - FileUtil.delete(file); - FileUtil.delete(fileBa); + tempDir.cleanup(); } @Test void close() { assertThatThrownBy( () -> { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .addDbiFlag(MDB_CREATE) + .open(); db.put(bb(1), bb(42)); db.close(); db.put(bb(2), bb(42)); // error @@ -105,6 +120,78 @@ void close() { .isInstanceOf(ConstantDerivedException.class); } + @Test + void constructorArgsNullEnv() { + assertThatThrownBy( + () -> { + try (Txn txn = env.txnWrite()) { + new Dbi<>( + null, + txn, + "foo".getBytes(Env.DEFAULT_NAME_CHARSET), + PROXY_OPTIMAL, + DbiFlagSet.EMPTY); + } + }) + .isInstanceOf(NullPointerException.class); + } + + @Test + void constructorArgsNullTxn() { + assertThatThrownBy( + () -> { + new Dbi<>( + env, + null, + "foo".getBytes(Env.DEFAULT_NAME_CHARSET), + PROXY_OPTIMAL, + DbiFlagSet.EMPTY); + }) + .isInstanceOf(NullPointerException.class); + } + + @Test + void constructorArgsNullProxy() { + assertThatThrownBy( + () -> { + try (Txn txn = env.txnWrite()) { + new Dbi<>( + env, txn, "foo".getBytes(Env.DEFAULT_NAME_CHARSET), null, DbiFlagSet.EMPTY); + } + }) + .isInstanceOf(NullPointerException.class); + } + + @Test + void constructorArgsNullFlags() { + assertThatThrownBy( + () -> { + try (Txn txn = env.txnWrite()) { + new Dbi<>(env, txn, "foo".getBytes(Env.DEFAULT_NAME_CHARSET), PROXY_OPTIMAL, null); + } + }) + .isInstanceOf(NullPointerException.class); + } + + @Test + void constructorArgsNullNativeCallbackComparator() { + assertThatThrownBy( + () -> { + try (Txn txn = env.txnWrite()) { + new Dbi<>( + env, + txn, + "foo".getBytes(Env.DEFAULT_NAME_CHARSET), + null, + true, + PROXY_OPTIMAL, + DbiFlagSet.EMPTY); + } + }) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Is nativeCb is true, you must supply a comparator"); + } + @Test void customComparator() { final Comparator reverseOrder = @@ -136,7 +223,12 @@ private void doCustomComparator( Comparator comparator, IntFunction serializer, ToIntFunction deserializer) { - final Dbi db = env.openDbi(DB_1, comparator, true, MDB_CREATE); + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withCallbackComparator(ignored -> comparator) + .setDbiFlags(MDB_CREATE) + .open(); try (Txn txn = env.txnWrite()) { assertThat(db.put(txn, serializer.apply(2), serializer.apply(3))).isTrue(); assertThat(db.put(txn, serializer.apply(4), serializer.apply(6))).isTrue(); @@ -157,9 +249,21 @@ private void doCustomComparator( void dbOpenMaxDatabases() { assertThatThrownBy( () -> { - env.openDbi("db1 is OK", MDB_CREATE); - env.openDbi("db2 is OK", MDB_CREATE); - env.openDbi("db3 fails", MDB_CREATE); + env.createDbi() + .setDbName("db1 is OK") + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + env.createDbi() + .setDbName("db2 is OK") + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + env.createDbi() + .setDbName("db3 fails") + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); }) .isInstanceOf(DbFullException.class); } @@ -178,16 +282,23 @@ 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 Dbi db = env.openDbi(DB_1, c, true, flags); + final DbiFlagSet flags = DbiFlagSet.of(MDB_CREATE, MDB_INTEGERKEY); + final Comparator comparator = comparatorSupplier.get(); + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withCallbackComparator(ignored -> comparator) + .setDbiFlags(flags) + .open(); final List keys = range(0, 1_000).boxed().collect(toList()); - final ExecutorService pool = Executors.newCachedThreadPool(); + // TODO surround with try-with-resources in J19+ + //noinspection resource // Not in J8 + ExecutorService pool = Executors.newCachedThreadPool(); final AtomicBoolean proceed = new AtomicBoolean(true); final Future reader = pool.submit( @@ -229,7 +340,8 @@ private void doDbiWithComparatorThreadSafety( @Test void drop() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Txn txn = env.txnWrite()) { db.put(txn, bb(1), bb(42)); db.put(txn, bb(2), bb(42)); @@ -247,8 +359,14 @@ void drop() { @Test void dropAndDelete() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - final Dbi nameDb = env.openDbi((byte[]) null); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + final Dbi nameDb = + env.createDbi() + .setDbName((byte[]) null) + .withDefaultComparator() + .setDbiFlags(DbiFlagSet.EMPTY) + .open(); final byte[] dbNameBytes = DB_1.getBytes(UTF_8); final ByteBuffer dbNameBuffer = allocateDirect(dbNameBytes.length); dbNameBuffer.put(dbNameBytes).flip(); @@ -263,8 +381,8 @@ void dropAndDelete() { @Test void dropAndDeleteAnonymousDb() { - env.openDbi(DB_1, MDB_CREATE); - final Dbi nameDb = env.openDbi((byte[]) null); + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + final Dbi nameDb = env.createDbi().withoutDbName().withDefaultComparator().open(); final byte[] dbNameBytes = DB_1.getBytes(UTF_8); final ByteBuffer dbNameBuffer = allocateDirect(dbNameBytes.length); dbNameBuffer.put(dbNameBytes).flip(); @@ -281,7 +399,8 @@ void dropAndDeleteAnonymousDb() { @Test void getName() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); assertThat(db.getName()).isEqualTo(DB_1.getBytes(UTF_8)); } @@ -289,8 +408,8 @@ void getName() { void getNamesWhenDbisPresent() { 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); + env.createDbi().setDbName(dbHello).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + env.createDbi().setDbName(dbWorld).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); final List dbiNames = env.getDbiNames(); assertThat(dbiNames).hasSize(2); assertThat(dbiNames.get(0)).isEqualTo(dbHello); @@ -305,7 +424,12 @@ void getNamesWhenEmpty() { @Test void listsFlags() { - final Dbi dbi = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT, MDB_REVERSEKEY); + final Dbi dbi = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE, MDB_DUPSORT, MDB_REVERSEKEY) + .open(); try (Txn txn = env.txnRead()) { final List flags = dbi.listFlags(txn); @@ -315,7 +439,8 @@ void listsFlags() { @Test void putAbortGet() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Txn txn = env.txnWrite()) { db.put(txn, bb(5), bb(5)); @@ -329,7 +454,8 @@ void putAbortGet() { @Test void putAndGetAndDeleteWithInternalTx() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); db.put(bb(5), bb(5)); try (Txn txn = env.txnRead()) { @@ -347,7 +473,8 @@ void putAndGetAndDeleteWithInternalTx() { @Test void putCommitGet() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Txn txn = env.txnWrite()) { db.put(txn, bb(5), bb(5)); txn.commit(); @@ -362,31 +489,32 @@ void putCommitGet() { @Test void putCommitGetByteArray() { - FileUtil.useTempFile( - file -> { - 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()) { - db.put(txn, ba(5), ba(5)); - txn.commit(); - } - try (Txn txn = envBa.txnWrite()) { - final byte[] found = db.get(txn, ba(5)); - assertThat(found).isNotNull(); - assertThat(fromBa(txn.val())).isEqualTo(5); - } - } - }); + final Path file = tempDir.createTempFile(); + try (Env envBa = + create(PROXY_BA) + .setMapSize(64, ByteUnit.MEBIBYTES) + .setMaxReaders(1) + .setMaxDbs(2) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { + final Dbi db = + envBa.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + try (Txn txn = envBa.txnWrite()) { + db.put(txn, ba(5), ba(5)); + txn.commit(); + } + try (Txn txn = envBa.txnWrite()) { + final byte[] found = db.get(txn, ba(5)); + assertThat(found).isNotNull(); + assertThat(fromBa(txn.val())).isEqualTo(5); + } + } } @Test void putDelete() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Txn txn = env.txnWrite()) { db.put(txn, bb(5), bb(5)); @@ -399,7 +527,12 @@ void putDelete() { @Test void putDuplicateDelete() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE, MDB_DUPSORT) + .open(); try (Txn txn = env.txnWrite()) { db.put(txn, bb(5), bb(5)); @@ -421,7 +554,8 @@ void putDuplicateDelete() { @Test void putReserve() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); final ByteBuffer key = bb(5); try (Txn txn = env.txnWrite()) { @@ -442,7 +576,8 @@ void putReserve() { @Test void putZeroByteValueForNonMdbDupSortDatabase() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Txn txn = env.txnWrite()) { final ByteBuffer val = allocateDirect(0); db.put(txn, bb(5), val); @@ -458,7 +593,12 @@ void putZeroByteValueForNonMdbDupSortDatabase() { @Test void returnValueForNoDupData() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE, MDB_DUPSORT) + .open(); try (Txn txn = env.txnWrite()) { // ok assertThat(db.put(txn, bb(5), bb(6), MDB_NODUPDATA)).isTrue(); @@ -469,7 +609,8 @@ void returnValueForNoDupData() { @Test void returnValueForNoOverwrite() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Txn txn = env.txnWrite()) { // ok assertThat(db.put(txn, bb(5), bb(6), MDB_NOOVERWRITE)).isTrue(); @@ -481,7 +622,8 @@ void returnValueForNoOverwrite() { @Test void stats() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); db.put(bb(1), bb(42)); db.put(bb(2), bb(42)); db.put(bb(3), bb(42)); @@ -502,7 +644,12 @@ void stats() { void testMapFullException() { assertThatThrownBy( () -> { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); try (Txn txn = env.txnWrite()) { final ByteBuffer v; try { @@ -523,7 +670,8 @@ void testParallelWritesStress() { return; // Windows VMs run this test too slowly } - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); // Travis CI has 1.5 cores for legacy builds nCopies(2, null).parallelStream() @@ -540,7 +688,11 @@ void closedEnvRejectsOpenCall() { assertThatThrownBy( () -> { env.close(); - env.openDbi(DB_1, MDB_CREATE); + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); }) .isInstanceOf(AlreadyClosedException.class); } @@ -561,6 +713,7 @@ void closedEnvRejectsGetCall() { doEnvClosedTest( (db, txn) -> { final ByteBuffer valBuf = db.get(txn, bb(1)); + assertThat(valBuf).isNotNull(); assertThat(valBuf.getInt()).isEqualTo(10); }, (db, txn) -> db.get(txn, bb(2))); @@ -647,7 +800,8 @@ void closedEnvRejectsStatCall() { private void doEnvClosedTest( final BiConsumer, Txn> workBeforeEnvClosed, final BiConsumer, Txn> workAfterEnvClose) { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); db.put(bb(1), bb(10)); db.put(bb(2), bb(20)); @@ -667,4 +821,12 @@ private void doEnvClosedTest( } } } + + @Test + void getNameBytes() { + //noinspection ConstantValue + assertThat(Dbi.getNameBytes(null)).isNull(); + ; + assertThat(Dbi.getNameBytes("foo")).isEqualTo("foo".getBytes(Env.DEFAULT_NAME_CHARSET)); + } } diff --git a/src/test/java/org/lmdbjava/DirectBufferProxyTest.java b/src/test/java/org/lmdbjava/DirectBufferProxyTest.java new file mode 100644 index 00000000..fc01e801 --- /dev/null +++ b/src/test/java/org/lmdbjava/DirectBufferProxyTest.java @@ -0,0 +1,129 @@ +/* + * Copyright © 2016-2026 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.ByteOrder; +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Random; +import java.util.Set; +import org.agrona.DirectBuffer; +import org.agrona.MutableDirectBuffer; +import org.agrona.concurrent.UnsafeBuffer; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class DirectBufferProxyTest { + + @Test + public void verifyComparators_int() { + final Random random = new Random(203948); + final MutableDirectBuffer buffer1native = new UnsafeBuffer(new byte[Integer.BYTES]); + final MutableDirectBuffer buffer2native = new UnsafeBuffer(new byte[Integer.BYTES]); + final MutableDirectBuffer buffer1be = new UnsafeBuffer(new byte[Integer.BYTES]); + final MutableDirectBuffer buffer2be = new UnsafeBuffer(new byte[Integer.BYTES]); + final int[] values = random.ints().filter(i -> i >= 0).limit(5_000_000).toArray(); + + final LinkedHashMap> comparators = new LinkedHashMap<>(); + comparators.put(CompareType.INTEGER_KEY, DirectBufferProxy::compareAsIntegerKeys); + comparators.put(CompareType.LEXICOGRAPHIC, DirectBufferProxy::compareLexicographically); + + final LinkedHashMap results = + new LinkedHashMap<>(comparators.size()); + final Set uniqueResults = new HashSet<>(comparators.size()); + + for (int i = 1; i < values.length; i++) { + final int val1 = values[i - 1]; + final int val2 = values[i]; + buffer1native.putInt(0, val1, ByteOrder.nativeOrder()); + buffer2native.putInt(0, val2, ByteOrder.nativeOrder()); + buffer1be.putInt(0, val1, ByteOrder.BIG_ENDIAN); + buffer2be.putInt(0, val2, ByteOrder.BIG_ENDIAN); + + uniqueResults.clear(); + + // Make sure all comparators give the same result for the same inputs + comparators.forEach( + (compareType, comparator) -> { + final ComparatorResult result; + // IntegerKey comparator expects keys to have been written in native order so need + // different buffers. + if (compareType == CompareType.INTEGER_KEY) { + result = TestUtils.compare(comparator, buffer1native, buffer2native); + } else { + result = TestUtils.compare(comparator, buffer1be, buffer2be); + } + results.put(compareType, result); + uniqueResults.add(result); + }); + + if (uniqueResults.size() != 1) { + Assertions.fail( + "Comparator mismatch for values: " + val1 + " and " + val2 + ". Results: " + results); + } + } + } + + @Test + public void verifyComparators_long() { + final Random random = new Random(203948); + final MutableDirectBuffer buffer1native = new UnsafeBuffer(new byte[Long.BYTES]); + final MutableDirectBuffer buffer2native = new UnsafeBuffer(new byte[Long.BYTES]); + final MutableDirectBuffer buffer1be = new UnsafeBuffer(new byte[Long.BYTES]); + final MutableDirectBuffer buffer2be = new UnsafeBuffer(new byte[Long.BYTES]); + final long[] values = random.longs().filter(i -> i >= 0).limit(5_000_000).toArray(); + + final LinkedHashMap> comparators = new LinkedHashMap<>(); + comparators.put(CompareType.INTEGER_KEY, DirectBufferProxy::compareAsIntegerKeys); + comparators.put(CompareType.LEXICOGRAPHIC, DirectBufferProxy::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, ByteOrder.nativeOrder()); + buffer2native.putLong(0, val2, ByteOrder.nativeOrder()); + buffer1be.putLong(0, val1, ByteOrder.BIG_ENDIAN); + buffer2be.putLong(0, val2, ByteOrder.BIG_ENDIAN); + + uniqueResults.clear(); + + // Make sure all comparators give the same result for the same inputs + comparators.forEach( + (compareType, comparator) -> { + final ComparatorResult result; + // IntegerKey comparator expects keys to have been written in native order so need + // different buffers. + if (compareType == CompareType.INTEGER_KEY) { + result = TestUtils.compare(comparator, buffer1native, buffer2native); + } else { + result = TestUtils.compare(comparator, buffer1be, buffer2be); + } + results.put(compareType, result); + uniqueResults.add(result); + }); + + if (uniqueResults.size() != 1) { + Assertions.fail( + "Comparator mismatch for values: " + val1 + " and " + val2 + ". Results: " + results); + } + } + } +} diff --git a/src/test/java/org/lmdbjava/EnvDeprecatedTest.java b/src/test/java/org/lmdbjava/EnvDeprecatedTest.java new file mode 100644 index 00000000..da004cc8 --- /dev/null +++ b/src/test/java/org/lmdbjava/EnvDeprecatedTest.java @@ -0,0 +1,383 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import static java.nio.ByteBuffer.allocateDirect; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.lmdbjava.ByteUnit.KIBIBYTES; +import static org.lmdbjava.ByteUnit.MEBIBYTES; +import static org.lmdbjava.CopyFlags.MDB_CP_COMPACT; +import static org.lmdbjava.DbiFlags.MDB_CREATE; +import static org.lmdbjava.Env.Builder.MAX_READERS_DEFAULT; +import static org.lmdbjava.EnvFlags.MDB_NOSUBDIR; +import static org.lmdbjava.EnvFlags.MDB_RDONLY_ENV; +import static org.lmdbjava.TestUtils.DB_1; +import static org.lmdbjava.TestUtils.bb; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Random; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.lmdbjava.Env.AlreadyClosedException; +import org.lmdbjava.Env.AlreadyOpenException; +import org.lmdbjava.Env.Builder; +import org.lmdbjava.Env.InvalidCopyDestination; +import org.lmdbjava.Env.MapFullException; + +/** + * Tests all the deprecated methods in {@link Env}. Essentially a duplicate of {@link EnvTest}. When + * all the deprecated methods are deleted we can delete this test class. + * + * @deprecated Tests all the deprecated methods in {@link Env}. + */ +@Deprecated +public class EnvDeprecatedTest { + + private TempDir tempDir; + + @BeforeEach + void beforeEach() { + tempDir = new TempDir(); + } + + @AfterEach + void afterEach() { + tempDir.cleanup(); + } + + @Test + void byteUnit() { + final Path file = tempDir.createTempFile(); + try (Env env = + Env.create() + .setMaxReaders(1) + .setMapSize(MEBIBYTES.toBytes(1)) + .open(file.toFile(), MDB_NOSUBDIR)) { + final EnvInfo info = env.info(); + assertThat(info.mapSize).isEqualTo(MEBIBYTES.toBytes(1)); + } + } + + @Test + void cannotChangeMapSizeAfterOpen() { + assertThatThrownBy( + () -> { + final Path file = tempDir.createTempFile(); + final Builder builder = Env.create().setMaxReaders(1); + try (Env env = builder.open(file.toFile(), MDB_NOSUBDIR)) { + builder.setMapSize(1); + } + }) + .isInstanceOf(AlreadyOpenException.class); + } + + @Test + void cannotChangeMaxDbsAfterOpen() { + assertThatThrownBy( + () -> { + final Path file = tempDir.createTempFile(); + final Builder builder = Env.create().setMaxReaders(1); + try (Env env = builder.open(file.toFile(), MDB_NOSUBDIR)) { + builder.setMaxDbs(1); + } + }) + .isInstanceOf(AlreadyOpenException.class); + } + + @Test + void cannotChangeMaxReadersAfterOpen() { + assertThatThrownBy( + () -> { + final Path file = tempDir.createTempFile(); + final Builder builder = Env.create().setMaxReaders(1); + try (Env env = builder.open(file.toFile(), MDB_NOSUBDIR)) { + builder.setMaxReaders(1); + } + }) + .isInstanceOf(AlreadyOpenException.class); + } + + @Test + void cannotInfoOnceClosed() { + assertThatThrownBy( + () -> { + final Path file = tempDir.createTempFile(); + final Env env = + Env.create().setMaxReaders(1).open(file.toFile(), MDB_NOSUBDIR); + env.close(); + env.info(); + }) + .isInstanceOf(AlreadyClosedException.class); + } + + @Test + void cannotOpenTwice() { + assertThatThrownBy( + () -> { + final Path file = tempDir.createTempFile(); + final Builder builder = Env.create().setMaxReaders(1); + builder.open(file.toFile(), MDB_NOSUBDIR).close(); + builder.open(file.toFile(), MDB_NOSUBDIR); + }) + .isInstanceOf(AlreadyOpenException.class); + } + + @Test + void cannotStatOnceClosed() { + assertThatThrownBy( + () -> { + final Path file = tempDir.createTempFile(); + final Env env = + Env.create().setMaxReaders(1).open(file.toFile(), MDB_NOSUBDIR); + env.close(); + env.stat(); + }) + .isInstanceOf(AlreadyClosedException.class); + } + + @Test + void cannotSyncOnceClosed() { + assertThatThrownBy( + () -> { + final Path file = tempDir.createTempFile(); + final Env env = + Env.create().setMaxReaders(1).open(file.toFile(), MDB_NOSUBDIR); + env.close(); + env.sync(false); + }) + .isInstanceOf(AlreadyClosedException.class); + } + + @Test + void copyDirectoryBased() { + final Path dest = tempDir.createTempDir(); + final Path src = tempDir.createTempDir(); + assertThat(Files.exists(dest)).isTrue(); + assertThat(Files.isDirectory(dest)).isTrue(); + assertThat(FileUtil.count(dest)).isEqualTo(0); + try (Env env = Env.create().setMaxReaders(1).open(src.toFile())) { + env.copy(dest.toFile(), MDB_CP_COMPACT); + assertThat(FileUtil.count(dest)).isEqualTo(1); + } + } + + @Test + void copyDirectoryRejectsFileDestination() { + assertThatThrownBy( + () -> { + final Path dest = tempDir.createTempDir(); + final Path src = tempDir.createTempDir(); + FileUtil.deleteDir(dest); + try (Env env = Env.create().setMaxReaders(1).open(src.toFile())) { + env.copy(dest.toFile(), MDB_CP_COMPACT); + } + }) + .isInstanceOf(InvalidCopyDestination.class); + } + + @Test + void copyDirectoryRejectsMissingDestination() { + final Path dest = tempDir.createTempDir(); + final Path src = tempDir.createTempDir(); + assertThatThrownBy( + () -> { + try { + Files.delete(dest); + try (Env env = Env.create().setMaxReaders(1).open(src.toFile())) { + env.copy(dest.toFile(), MDB_CP_COMPACT); + } + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + }) + .isInstanceOf(InvalidCopyDestination.class); + } + + @Test + void copyDirectoryRejectsNonEmptyDestination() { + final Path dest = tempDir.createTempDir(); + final Path src = tempDir.createTempDir(); + assertThatThrownBy( + () -> { + try { + final Path subDir = dest.resolve("hello"); + Files.createDirectory(subDir); + assertThat(Files.isDirectory(subDir)).isTrue(); + try (Env env = Env.create().setMaxReaders(1).open(src.toFile())) { + env.copy(dest.toFile(), MDB_CP_COMPACT); + } + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + }) + .isInstanceOf(InvalidCopyDestination.class); + } + + @Test + void copyFileBased() { + final Path dest = tempDir.createTempFile(); + final Path src = tempDir.createTempFile(); + assertThat(Files.exists(dest)).isFalse(); + try (Env env = Env.create().setMaxReaders(1).open(src.toFile(), MDB_NOSUBDIR)) { + env.copy(dest.toFile(), MDB_CP_COMPACT); + } + assertThat(FileUtil.size(dest)).isGreaterThan(0L); + } + + @Test + void copyFileRejectsExistingDestination() { + final Path dest = tempDir.createTempFile(); + final Path src = tempDir.createTempFile(); + assertThatThrownBy( + () -> { + Files.createFile(dest); + assertThat(Files.exists(dest)).isTrue(); + try (Env env = + Env.create().setMaxReaders(1).open(src.toFile(), MDB_NOSUBDIR)) { + env.copy(dest.toFile(), MDB_CP_COMPACT); + } + }) + .isInstanceOf(InvalidCopyDestination.class); + } + + @Test + void createAsFile() { + final Path file = tempDir.createTempFile(); + try (Env env = + Env.create() + .setMapSize(MEBIBYTES.toBytes(1)) + .setMaxDbs(1) + .setMaxReaders(1) + .open(file.toFile(), MDB_NOSUBDIR)) { + env.sync(true); + assertThat(Files.isRegularFile(file)).isTrue(); + } + } + + @Test + void mapFull() { + final Path dir = tempDir.createTempDir(); + assertThatThrownBy( + () -> { + final byte[] k = new byte[500]; + final ByteBuffer key = allocateDirect(500); + final ByteBuffer val = allocateDirect(1_024); + final Random rnd = new Random(); + try (Env env = + Env.create() + .setMaxReaders(1) + .setMapSize(MEBIBYTES.toBytes(8)) + .setMaxDbs(1) + .open(dir.toFile())) { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + for (; ; ) { + rnd.nextBytes(k); + key.clear(); + key.put(k).flip(); + val.clear(); + db.put(key, val); + } + } + }) + .isInstanceOf(MapFullException.class); + } + + @Test + void readOnlySupported() { + final Path dir = tempDir.createTempDir(); + try (Env rwEnv = Env.create().setMaxReaders(1).open(dir.toFile())) { + final Dbi rwDb = rwEnv.openDbi(DB_1, MDB_CREATE); + rwDb.put(bb(1), bb(42)); + } + try (Env roEnv = Env.create().setMaxReaders(1).open(dir.toFile(), MDB_RDONLY_ENV)) { + final Dbi roDb = roEnv.openDbi(DB_1); + try (Txn roTxn = roEnv.txnRead()) { + assertThat(roDb.get(roTxn, bb(1))).isNotNull(); + } + } + } + + @Test + void setMapSize() { + final Path dir = tempDir.createTempDir(); + final byte[] k = new byte[500]; + final ByteBuffer key = allocateDirect(500); + final ByteBuffer val = allocateDirect(1_024); + final Random rnd = new Random(); + try (Env env = + Env.create() + .setMaxReaders(1) + .setMapSize(KIBIBYTES.toBytes(256)) + .setMaxDbs(1) + .open(dir.toFile())) { + final Dbi db = env.openDbi(DB_1, MDB_CREATE); + + db.put(bb(1), bb(42)); + boolean mapFullExThrown = false; + try { + for (int i = 0; i < 70; i++) { + rnd.nextBytes(k); + key.clear(); + key.put(k).flip(); + val.clear(); + db.put(key, val); + } + } catch (final MapFullException mfE) { + mapFullExThrown = true; + } + assertThat(mapFullExThrown).isTrue(); + + env.setMapSize(KIBIBYTES.toBytes(1024)); + + try (Txn roTxn = env.txnRead()) { + final ByteBuffer byteBuffer = db.get(roTxn, bb(1)); + assertThat(byteBuffer).isNotNull(); + assertThat(byteBuffer.getInt()).isEqualTo(42); + } + + mapFullExThrown = false; + try { + for (int i = 0; i < 70; i++) { + rnd.nextBytes(k); + key.clear(); + key.put(k).flip(); + val.clear(); + db.put(key, val); + } + } catch (final MapFullException mfE) { + mapFullExThrown = true; + } + assertThat(mapFullExThrown).isFalse(); + } + } + + @Test + void testDefaultOpen() { + final Path dir = tempDir.createTempDir(); + try (Env env = Env.open(dir.toFile(), 10)) { + final EnvInfo info = env.info(); + assertThat(info.maxReaders).isEqualTo(MAX_READERS_DEFAULT); + final Dbi db = env.openDbi("test", MDB_CREATE); + db.put(allocateDirect(1), allocateDirect(1)); + } + } +} diff --git a/src/test/java/org/lmdbjava/EnvFlagSetTest.java b/src/test/java/org/lmdbjava/EnvFlagSetTest.java new file mode 100644 index 00000000..a06334af --- /dev/null +++ b/src/test/java/org/lmdbjava/EnvFlagSetTest.java @@ -0,0 +1,89 @@ +/* + * 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.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class EnvFlagSetTest extends AbstractFlagSetTest { + + @Test + void test() { + // This is here purely to stop CodeQL moaning that this class is unused. + // All the actual tests are in the superclass + Assertions.assertThat(getAllFlags()).isNotNull(); + } + + @Override + List getAllFlags() { + return Arrays.stream(EnvFlags.values()).collect(Collectors.toList()); + } + + @Override + EnvFlagSet getEmptyFlagSet() { + return EnvFlagSet.empty(); + } + + @Override + AbstractFlagSet.Builder getBuilder() { + return EnvFlagSet.builder(); + } + + @Override + EnvFlagSet getFlagSet(Collection flags) { + return EnvFlagSet.of(flags); + } + + @Override + EnvFlagSet getFlagSet(EnvFlags[] flags) { + return EnvFlagSet.of(flags); + } + + @Override + EnvFlagSet getFlagSet(EnvFlags flag) { + return EnvFlagSet.of(flag); + } + + @Override + Class getFlagType() { + return EnvFlags.class; + } + + @Override + Function, EnvFlagSet> getConstructor() { + return EnvFlagSetImpl::new; + } + + /** + * {@link FlagSet#isSet(MaskedFlag)} on the flag enum is tested in {@link AbstractFlagSetTest} but + * the coverage check doesn't seem to notice it. + */ + @Test + void testIsSet() { + assertThat(EnvFlags.MDB_RDONLY_ENV.isSet(EnvFlags.MDB_RDONLY_ENV)).isTrue(); + assertThat(EnvFlags.MDB_RDONLY_ENV.isSet(EnvFlags.MDB_WRITEMAP)).isFalse(); + //noinspection ConstantValue + assertThat(EnvFlags.MDB_RDONLY_ENV.isSet(null)).isFalse(); + } +} diff --git a/src/test/java/org/lmdbjava/EnvTest.java b/src/test/java/org/lmdbjava/EnvTest.java index 87edd049..69a5ea4e 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -16,17 +16,15 @@ package org.lmdbjava; -import static com.jakewharton.byteunits.BinaryByteUnit.KIBIBYTES; -import static com.jakewharton.byteunits.BinaryByteUnit.MEBIBYTES; import static java.nio.ByteBuffer.allocateDirect; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.lmdbjava.CopyFlags.MDB_CP_COMPACT; import static org.lmdbjava.DbiFlags.MDB_CREATE; import static org.lmdbjava.Env.Builder.MAX_READERS_DEFAULT; -import static org.lmdbjava.Env.create; -import static org.lmdbjava.Env.open; import static org.lmdbjava.EnvFlags.MDB_NOSUBDIR; +import static org.lmdbjava.EnvFlags.MDB_NOSYNC; +import static org.lmdbjava.EnvFlags.MDB_NOTLS; import static org.lmdbjava.EnvFlags.MDB_RDONLY_ENV; import static org.lmdbjava.TestUtils.DB_1; import static org.lmdbjava.TestUtils.bb; @@ -36,7 +34,14 @@ import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Random; +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.lmdbjava.Env.AlreadyClosedException; import org.lmdbjava.Env.AlreadyOpenException; @@ -48,32 +53,56 @@ /** Test {@link Env}. */ public final class EnvTest { + private TempDir tempDir; + + @BeforeEach + void beforeEach() { + tempDir = new TempDir(); + } + + @AfterEach + void afterEach() { + tempDir.cleanup(); + } + @Test void byteUnit() { - FileUtil.useTempFile( - file -> { - try (Env env = - create() - .setMaxReaders(1) - .setMapSize(MEBIBYTES.toBytes(1)) - .open(file.toFile(), MDB_NOSUBDIR)) { - final EnvInfo info = env.info(); - assertThat(info.mapSize).isEqualTo(MEBIBYTES.toBytes(1)); - } - }); + final Path file = tempDir.createTempFile(); + try (Env env = + Env.create() + .setMaxReaders(1) + .setMapSize(1, ByteUnit.MEBIBYTES) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { + final EnvInfo info = env.info(); + assertThat(info.mapSize).isEqualTo(ByteUnit.MEBIBYTES.toBytes(1)); + } } @Test void cannotChangeMapSizeAfterOpen() { assertThatThrownBy( () -> { - FileUtil.useTempFile( - file -> { - final Builder builder = create().setMaxReaders(1); - try (Env env = builder.open(file.toFile(), MDB_NOSUBDIR)) { - builder.setMapSize(1); - } - }); + final Path file = tempDir.createTempFile(); + final Builder builder = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); + try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { + builder.setMapSize(1); + } + }) + .isInstanceOf(AlreadyOpenException.class); + } + + @Test + void cannotChangePermissionsAfterOpen() { + assertThatThrownBy( + () -> { + final Path file = tempDir.createTempFile(); + final Builder builder = + Env.create().setFilePermissions(0666).setEnvFlags(MDB_NOSUBDIR); + try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { + builder.setFilePermissions(0664); + } }) .isInstanceOf(AlreadyOpenException.class); } @@ -82,13 +111,12 @@ void cannotChangeMapSizeAfterOpen() { void cannotChangeMaxDbsAfterOpen() { assertThatThrownBy( () -> { - FileUtil.useTempFile( - file -> { - final Builder builder = create().setMaxReaders(1); - try (Env env = builder.open(file.toFile(), MDB_NOSUBDIR)) { - builder.setMaxDbs(1); - } - }); + final Path file = tempDir.createTempFile(); + final Builder builder = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); + try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { + builder.setMaxDbs(1); + } }) .isInstanceOf(AlreadyOpenException.class); } @@ -97,13 +125,12 @@ void cannotChangeMaxDbsAfterOpen() { void cannotChangeMaxReadersAfterOpen() { assertThatThrownBy( () -> { - FileUtil.useTempFile( - file -> { - final Builder builder = create().setMaxReaders(1); - try (Env env = builder.open(file.toFile(), MDB_NOSUBDIR)) { - builder.setMaxReaders(1); - } - }); + final Path file = tempDir.createTempFile(); + final Builder builder = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); + try (Env ignored = builder.setEnvFlags(MDB_NOSUBDIR).open(file)) { + builder.setMaxReaders(1); + } }) .isInstanceOf(AlreadyOpenException.class); } @@ -112,13 +139,11 @@ void cannotChangeMaxReadersAfterOpen() { void cannotInfoOnceClosed() { assertThatThrownBy( () -> { - FileUtil.useTempFile( - file -> { - final Env env = - create().setMaxReaders(1).open(file.toFile(), MDB_NOSUBDIR); - env.close(); - env.info(); - }); + final Path file = tempDir.createTempFile(); + final Env env = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); + env.close(); + env.info(); }) .isInstanceOf(AlreadyClosedException.class); } @@ -127,12 +152,12 @@ void cannotInfoOnceClosed() { void cannotOpenTwice() { assertThatThrownBy( () -> { - FileUtil.useTempFile( - file -> { - final Builder builder = create().setMaxReaders(1); - builder.open(file.toFile(), MDB_NOSUBDIR).close(); - builder.open(file.toFile(), MDB_NOSUBDIR); - }); + final Path file = tempDir.createTempFile(); + final Builder builder = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR); + builder.open(file).close(); + //noinspection resource // This will fail to open + builder.open(file); }) .isInstanceOf(AlreadyOpenException.class); } @@ -141,25 +166,44 @@ void cannotOpenTwice() { void cannotOverflowMapSize() { assertThatThrownBy( () -> { - final Builder builder = create().setMaxReaders(1); + final Builder builder = Env.create().setMaxReaders(1); final int mb = 1_024 * 1_024; + //noinspection NumericOverflow // Intentional overflow final int size = mb * 2_048; // as per issue 18 builder.setMapSize(size); }) .isInstanceOf(IllegalArgumentException.class); } + @Test + void negativeMapSize() { + assertThatThrownBy( + () -> { + final Builder builder = Env.create().setMaxReaders(1); + builder.setMapSize(-1); + }) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void negativeMapSize2() { + assertThatThrownBy( + () -> { + final Builder builder = Env.create().setMaxReaders(1); + builder.setMapSize(-1, ByteUnit.MEBIBYTES); + }) + .isInstanceOf(IllegalArgumentException.class); + } + @Test void cannotStatOnceClosed() { assertThatThrownBy( () -> { - FileUtil.useTempFile( - file -> { - final Env env = - create().setMaxReaders(1).open(file.toFile(), MDB_NOSUBDIR); - env.close(); - env.stat(); - }); + final Path file = tempDir.createTempFile(); + final Env env = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); + env.close(); + env.stat(); }) .isInstanceOf(AlreadyClosedException.class); } @@ -168,48 +212,51 @@ void cannotStatOnceClosed() { void cannotSyncOnceClosed() { assertThatThrownBy( () -> { - FileUtil.useTempFile( - file -> { - final Env env = - create().setMaxReaders(1).open(file.toFile(), MDB_NOSUBDIR); - env.close(); - env.sync(false); - }); + final Path file = tempDir.createTempFile(); + final Env env = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); + env.close(); + env.sync(false); }) .isInstanceOf(AlreadyClosedException.class); } @Test void copyDirectoryBased() { - FileUtil.useTempDir( - dest -> { - assertThat(Files.exists(dest)).isTrue(); - assertThat(Files.isDirectory(dest)).isTrue(); - assertThat(FileUtil.count(dest)).isEqualTo(0); - FileUtil.useTempDir( - src -> { - try (Env env = create().setMaxReaders(1).open(src.toFile())) { - env.copy(dest.toFile(), MDB_CP_COMPACT); - assertThat(FileUtil.count(dest)).isEqualTo(1); - } - }); - }); + final Path dest = tempDir.createTempDir(); + assertThat(Files.exists(dest)).isTrue(); + assertThat(Files.isDirectory(dest)).isTrue(); + assertThat(FileUtil.count(dest)).isEqualTo(0); + final Path src = tempDir.createTempDir(); + try (Env env = Env.create().setMaxReaders(1).open(src)) { + env.copy(dest, MDB_CP_COMPACT); + assertThat(FileUtil.count(dest)).isEqualTo(1); + } + } + + @Test + void copyDirectoryBased_noFlags() { + final Path dest = tempDir.createTempDir(); + assertThat(Files.exists(dest)).isTrue(); + assertThat(Files.isDirectory(dest)).isTrue(); + assertThat(FileUtil.count(dest)).isEqualTo(0); + final Path src = tempDir.createTempDir(); + try (Env env = Env.create().setMaxReaders(1).open(src)) { + env.copy(dest); + assertThat(FileUtil.count(dest)).isEqualTo(1); + } } @Test void copyDirectoryRejectsFileDestination() { assertThatThrownBy( () -> { - FileUtil.useTempDir( - dest -> { - FileUtil.deleteDir(dest); - FileUtil.useTempDir( - src -> { - try (Env env = create().setMaxReaders(1).open(src.toFile())) { - env.copy(dest.toFile(), MDB_CP_COMPACT); - } - }); - }); + final Path dest = tempDir.createTempDir(); + FileUtil.deleteDir(dest); + final Path src = tempDir.createTempDir(); + try (Env env = Env.create().setMaxReaders(1).open(src)) { + env.copy(dest, MDB_CP_COMPACT); + } }) .isInstanceOf(InvalidCopyDestination.class); } @@ -218,21 +265,16 @@ void copyDirectoryRejectsFileDestination() { void copyDirectoryRejectsMissingDestination() { assertThatThrownBy( () -> { - FileUtil.useTempDir( - dest -> { - try { - Files.delete(dest); - FileUtil.useTempDir( - src -> { - try (Env env = - create().setMaxReaders(1).open(src.toFile())) { - env.copy(dest.toFile(), MDB_CP_COMPACT); - } - }); - } catch (final IOException e) { - throw new UncheckedIOException(e); - } - }); + final Path dest = tempDir.createTempDir(); + try { + Files.delete(dest); + final Path src = tempDir.createTempDir(); + try (Env env = Env.create().setMaxReaders(1).open(src)) { + env.copy(dest, MDB_CP_COMPACT); + } + } catch (final IOException e) { + throw new UncheckedIOException(e); + } }) .isInstanceOf(InvalidCopyDestination.class); } @@ -241,260 +283,459 @@ void copyDirectoryRejectsMissingDestination() { void copyDirectoryRejectsNonEmptyDestination() { assertThatThrownBy( () -> { - FileUtil.useTempDir( - dest -> { - try { - final Path subDir = dest.resolve("hello"); - Files.createDirectory(subDir); - assertThat(Files.isDirectory(subDir)).isTrue(); - FileUtil.useTempDir( - src -> { - try (Env env = - create().setMaxReaders(1).open(src.toFile())) { - env.copy(dest.toFile(), MDB_CP_COMPACT); - } - }); - } catch (final IOException e) { - throw new UncheckedIOException(e); - } - }); + final Path dest = tempDir.createTempDir(); + try { + final Path subDir = dest.resolve("hello"); + Files.createDirectory(subDir); + assertThat(Files.isDirectory(subDir)).isTrue(); + final Path src = tempDir.createTempDir(); + try (Env env = Env.create().setMaxReaders(1).open(src)) { + env.copy(dest, MDB_CP_COMPACT); + } + } catch (final IOException e) { + throw new UncheckedIOException(e); + } }) .isInstanceOf(InvalidCopyDestination.class); } @Test void copyFileBased() { - FileUtil.useTempFile( - dest -> { - FileUtil.delete(dest); - assertThat(Files.exists(dest)).isFalse(); - FileUtil.useTempFile( - src -> { - try (Env env = - create().setMaxReaders(1).open(src.toFile(), MDB_NOSUBDIR)) { - env.copy(dest.toFile(), MDB_CP_COMPACT); - } - assertThat(FileUtil.size(dest)).isGreaterThan(0L); - }); - }); + final Path dest = tempDir.createTempFile(); + assertThat(Files.exists(dest)).isFalse(); + final Path src = tempDir.createTempFile(); + try (Env env = Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { + env.copy(dest, MDB_CP_COMPACT); + } + assertThat(FileUtil.size(dest)).isGreaterThan(0L); } @Test void copyFileRejectsExistingDestination() { assertThatThrownBy( () -> { - FileUtil.useTempFile( - dest -> { - assertThat(Files.exists(dest)).isTrue(); - FileUtil.useTempFile( - src -> { - try (Env env = - create().setMaxReaders(1).open(src.toFile(), MDB_NOSUBDIR)) { - env.copy(dest.toFile(), MDB_CP_COMPACT); - } - }); - }); + final Path dest = tempDir.createTempFile(); + Files.createFile(dest); + assertThat(Files.exists(dest)).isTrue(); + final Path src = tempDir.createTempFile(); + try (Env env = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(src)) { + env.copy(dest, MDB_CP_COMPACT); + } }) .isInstanceOf(InvalidCopyDestination.class); } @Test void createAsDirectory() { - FileUtil.useTempDir( - dest -> { - final Env env = create().setMaxReaders(1).open(dest.toFile()); - assertThat(Files.isDirectory(dest)).isTrue(); - env.sync(false); - env.close(); - assertThat(env.isClosed()).isTrue(); - env.close(); // safe to repeat - }); + final Path dest = tempDir.createTempDir(); + final Env env = Env.create().setMaxReaders(1).open(dest); + assertThat(Files.isDirectory(dest)).isTrue(); + env.sync(false); + env.close(); + assertThat(env.isClosed()).isTrue(); + env.close(); // safe to repeat } @Test void createAsFile() { - FileUtil.useTempFile( - file -> { - try (Env env = - create() - .setMapSize(MEBIBYTES.toBytes(1)) - .setMaxDbs(1) - .setMaxReaders(1) - .open(file.toFile(), MDB_NOSUBDIR)) { - env.sync(true); - assertThat(Files.isRegularFile(file)).isTrue(); - } - }); + final Path file = tempDir.createTempFile(); + try (Env env = + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { + env.sync(true); + assertThat(Files.isRegularFile(file)).isTrue(); + } } @Test void detectTransactionThreadViolation() { assertThatThrownBy( () -> { - FileUtil.useTempFile( - file -> { - try (Env env = - create().setMaxReaders(1).open(file.toFile(), MDB_NOSUBDIR)) { - env.txnRead(); - env.txnRead(); - } - }); + final Path file = tempDir.createTempFile(); + try (Env env = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file)) { + env.txnRead(); + env.txnRead(); + } }) .isInstanceOf(BadReaderLockException.class); } @Test void info() { - FileUtil.useTempFile( - file -> { - try (Env env = - create().setMaxReaders(4).setMapSize(123_456).open(file.toFile(), MDB_NOSUBDIR)) { - final EnvInfo info = env.info(); - assertThat(info).isNotNull(); - assertThat(info.lastPageNumber).isEqualTo(1L); - assertThat(info.lastTransactionId).isEqualTo(0L); - assertThat(info.mapAddress).isEqualTo(0L); - assertThat(info.mapSize).isEqualTo(123_456L); - assertThat(info.maxReaders).isEqualTo(4); - assertThat(info.numReaders).isEqualTo(0); - assertThat(info.toString()).contains("maxReaders="); - assertThat(env.getMaxKeySize()).isEqualTo(511); - } - }); + final Path file = tempDir.createTempFile(); + try (Env env = + Env.create() + .setMaxReaders(4) + .setMapSize(123_456) + .setEnvFlags(MDB_NOSUBDIR) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { + final EnvInfo info = env.info(); + assertThat(info).isNotNull(); + assertThat(info.lastPageNumber).isEqualTo(1L); + assertThat(info.lastTransactionId).isEqualTo(0L); + assertThat(info.mapAddress).isEqualTo(0L); + assertThat(info.mapSize).isEqualTo(123_456L); + assertThat(info.maxReaders).isEqualTo(4); + assertThat(info.numReaders).isEqualTo(0); + assertThat(info.toString()).contains("maxReaders="); + assertThat(env.getMaxKeySize()).isEqualTo(511); + } } @Test void mapFull() { assertThatThrownBy( () -> { - FileUtil.useTempDir( - dir -> { - final byte[] k = new byte[500]; - final ByteBuffer key = allocateDirect(500); - final ByteBuffer val = allocateDirect(1_024); - final Random rnd = new Random(); - try (Env env = - create() - .setMaxReaders(1) - .setMapSize(MEBIBYTES.toBytes(8)) - .setMaxDbs(1) - .open(dir.toFile())) { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - for (; ; ) { - rnd.nextBytes(k); - key.clear(); - key.put(k).flip(); - val.clear(); - db.put(key, val); - } - } - }); + final Path dir = tempDir.createTempDir(); + final byte[] k = new byte[500]; + final ByteBuffer key = allocateDirect(500); + final ByteBuffer val = allocateDirect(1_024); + final Random rnd = new Random(); + try (Env env = + Env.create() + .setMaxReaders(1) + .setMapSize(8, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .open(dir)) { + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + //noinspection InfiniteLoopStatement // Needs infinite loop to fill the env + for (; ; ) { + rnd.nextBytes(k); + key.clear(); + key.put(k).flip(); + val.clear(); + db.put(key, val); + } + } }) .isInstanceOf(MapFullException.class); } @Test void readOnlySupported() { - FileUtil.useTempDir( - dir -> { - try (Env rwEnv = create().setMaxReaders(1).open(dir.toFile())) { - final Dbi rwDb = rwEnv.openDbi(DB_1, MDB_CREATE); - rwDb.put(bb(1), bb(42)); - } - try (Env roEnv = - create().setMaxReaders(1).open(dir.toFile(), MDB_RDONLY_ENV)) { - final Dbi roDb = roEnv.openDbi(DB_1); - try (Txn roTxn = roEnv.txnRead()) { - assertThat(roDb.get(roTxn, bb(1))).isNotNull(); - } - } - }); + final Path dir = tempDir.createTempDir(); + try (Env rwEnv = Env.create().setMaxReaders(1).open(dir)) { + final Dbi rwDb = + rwEnv.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + rwDb.put(bb(1), bb(42)); + } + try (Env roEnv = + Env.create().setMaxReaders(1).setEnvFlags(MDB_RDONLY_ENV).open(dir)) { + final Dbi roDb = + roEnv + .createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(DbiFlagSet.EMPTY) + .open(); + try (Txn roTxn = roEnv.txnRead()) { + assertThat(roDb.get(roTxn, bb(1))).isNotNull(); + } + } } @Test void setMapSize() { - FileUtil.useTempDir( - dir -> { - final byte[] k = new byte[500]; - final ByteBuffer key = allocateDirect(500); - final ByteBuffer val = allocateDirect(1_024); - final Random rnd = new Random(); - try (Env env = - create() - .setMaxReaders(1) - .setMapSize(KIBIBYTES.toBytes(256)) - .setMaxDbs(1) - .open(dir.toFile())) { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - - db.put(bb(1), bb(42)); - boolean mapFullExThrown = false; - try { - for (int i = 0; i < 70; i++) { - rnd.nextBytes(k); - key.clear(); - key.put(k).flip(); - val.clear(); - db.put(key, val); - } - } catch (final MapFullException mfE) { - mapFullExThrown = true; - } - assertThat(mapFullExThrown).isTrue(); - - env.setMapSize(KIBIBYTES.toBytes(1024)); - - try (Txn roTxn = env.txnRead()) { - final ByteBuffer byteBuffer = db.get(roTxn, bb(1)); - assertThat(byteBuffer).isNotNull(); - assertThat(byteBuffer.getInt()).isEqualTo(42); - } - - mapFullExThrown = false; - try { - for (int i = 0; i < 70; i++) { - rnd.nextBytes(k); - key.clear(); - key.put(k).flip(); - val.clear(); - db.put(key, val); - } - } catch (final MapFullException mfE) { - mapFullExThrown = true; - } - assertThat(mapFullExThrown).isFalse(); - } - }); + final Path dir = tempDir.createTempDir(); + final byte[] k = new byte[500]; + final ByteBuffer key = allocateDirect(500); + final ByteBuffer val = allocateDirect(1_024); + final Random rnd = new Random(); + try (Env env = + Env.create().setMaxReaders(1).setMapSize(256, ByteUnit.KIBIBYTES).setMaxDbs(1).open(dir)) { + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + + db.put(bb(1), bb(42)); + boolean mapFullExThrown = false; + try { + for (int i = 0; i < 70; i++) { + rnd.nextBytes(k); + key.clear(); + key.put(k).flip(); + val.clear(); + db.put(key, val); + } + } catch (final MapFullException mfE) { + mapFullExThrown = true; + } + assertThat(mapFullExThrown).isTrue(); + + assertThatThrownBy( + () -> { + env.setMapSize(-1, ByteUnit.KIBIBYTES); + }) + .isInstanceOf(IllegalArgumentException.class); + + assertThatThrownBy( + () -> { + env.setMapSize(-1); + }) + .isInstanceOf(IllegalArgumentException.class); + + env.setMapSize(1024, ByteUnit.KIBIBYTES); + + try (Txn roTxn = env.txnRead()) { + final ByteBuffer byteBuffer = db.get(roTxn, bb(1)); + assertThat(byteBuffer).isNotNull(); + assertThat(byteBuffer.getInt()).isEqualTo(42); + } + + mapFullExThrown = false; + try { + for (int i = 0; i < 70; i++) { + rnd.nextBytes(k); + key.clear(); + key.put(k).flip(); + val.clear(); + db.put(key, val); + } + } catch (final MapFullException mfE) { + mapFullExThrown = true; + } + assertThat(mapFullExThrown).isFalse(); + } } @Test void stats() { - FileUtil.useTempFile( - file -> { - try (Env env = create().setMaxReaders(1).open(file.toFile(), MDB_NOSUBDIR)) { - final Stat stat = env.stat(); - assertThat(stat).isNotNull(); - assertThat(stat.branchPages).isEqualTo(0L); - assertThat(stat.depth).isEqualTo(0); - assertThat(stat.entries).isEqualTo(0L); - assertThat(stat.leafPages).isEqualTo(0L); - assertThat(stat.overflowPages).isEqualTo(0L); - assertThat(stat.pageSize % 4_096).isEqualTo(0); - assertThat(stat.toString()).contains("pageSize="); - } - }); + final Path file = tempDir.createTempFile(); + try (Env env = Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file)) { + final Stat stat = env.stat(); + assertThat(stat).isNotNull(); + assertThat(stat.branchPages).isEqualTo(0L); + assertThat(stat.depth).isEqualTo(0); + assertThat(stat.entries).isEqualTo(0L); + assertThat(stat.leafPages).isEqualTo(0L); + assertThat(stat.overflowPages).isEqualTo(0L); + assertThat(stat.pageSize % 4_096).isEqualTo(0); + assertThat(stat.toString()).contains("pageSize="); + } } @Test void testDefaultOpen() { - FileUtil.useTempDir( - dir -> { - try (Env env = open(dir.toFile(), 10)) { - final EnvInfo info = env.info(); - assertThat(info.maxReaders).isEqualTo(MAX_READERS_DEFAULT); - final Dbi db = env.openDbi("test", MDB_CREATE); - db.put(allocateDirect(1), allocateDirect(1)); - } - }); + final Path dir = tempDir.createTempDir(); + try (Env env = Env.create().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { + final EnvInfo info = env.info(); + assertThat(info.maxReaders).isEqualTo(MAX_READERS_DEFAULT); + final Dbi db = + env.createDbi().setDbName("test").withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + db.put(allocateDirect(1), allocateDirect(1)); + } + } + + @Test + void testDefaultOpenNoName1() { + final Path dir = tempDir.createTempDir(); + try (Env env = Env.create().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { + final EnvInfo info = env.info(); + assertThat(info.maxReaders).isEqualTo(MAX_READERS_DEFAULT); + final Dbi db = + env.createDbi() + .setDbName((String) null) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + db.put(bb("abc"), allocateDirect(1)); + db.put(bb("def"), allocateDirect(1)); + + // As this is the unnamed database it returns all keys in the unnamed db + final List dbiNamesBytes = env.getDbiNames(); + assertThat(dbiNamesBytes).hasSize(2); + assertThat(dbiNamesBytes.get(0)).isEqualTo("abc".getBytes(Env.DEFAULT_NAME_CHARSET)); + assertThat(dbiNamesBytes.get(1)).isEqualTo("def".getBytes(Env.DEFAULT_NAME_CHARSET)); + + final List dbiNames = env.getDbiNames(Env.DEFAULT_NAME_CHARSET); + assertThat(dbiNames).hasSize(2); + assertThat(dbiNames.get(0)).isEqualTo("abc"); + assertThat(dbiNames.get(1)).isEqualTo("def"); + } + } + + @Test + void testDefaultOpenNoName2() { + final Path dir = tempDir.createTempDir(); + try (Env env = Env.create().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { + final EnvInfo info = env.info(); + assertThat(info.maxReaders).isEqualTo(MAX_READERS_DEFAULT); + final Dbi db = + env.createDbi() + .setDbName((byte[]) null) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + db.put(bb("abc"), allocateDirect(1)); + db.put(bb("def"), allocateDirect(1)); + + // As this is the unnamed database it returns all keys in the unnamed db + final List dbiNames = env.getDbiNames(); + assertThat(dbiNames).hasSize(2); + assertThat(dbiNames.get(0)).isEqualTo("abc".getBytes(Env.DEFAULT_NAME_CHARSET)); + assertThat(dbiNames.get(1)).isEqualTo("def".getBytes(Env.DEFAULT_NAME_CHARSET)); + } + } + + @Test + void addEnvFlag() { + final Path file = tempDir.createTempFile(); + try (Env env = + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one + .open(file)) { + env.sync(true); + assertThat(Files.isRegularFile(file)).isTrue(); + assertThat(env.getEnvFlagSet().getFlags()) + .containsExactlyInAnyOrderElementsOf(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS).getFlags()); + } + } + + @Test + void addEnvFlags() { + final Path file = tempDir.createTempFile(); + try (Env env = + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlags(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS)) + .addEnvFlag(MDB_NOTLS) // Should not overwrite the existing one + .addEnvFlag(null) // no-op + .addEnvFlags((EnvFlagSet) null) // no-op + .addEnvFlags((Collection) null) // no-op + .open(file)) { + env.sync(true); + assertThat(env.getEnvFlagSet().getFlags()) + .containsExactlyInAnyOrderElementsOf(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS).getFlags()); + assertThat(Files.isRegularFile(file)).isTrue(); + } + } + + @Test + void addEnvFlags2() { + final Path file = tempDir.createTempFile(); + try (Env env = + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) + .addEnvFlags(Collections.singleton(MDB_NOSYNC)) + .open(file)) { + env.sync(true); + assertThat(env.getEnvFlagSet().getFlags()) + .containsExactlyInAnyOrderElementsOf( + EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS, MDB_NOSYNC).getFlags()); + assertThat(Files.isRegularFile(file)).isTrue(); + } + } + + @Test + void setEnvFlags() { + final Path file = tempDir.createTempFile(); + try (Env env = + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags((EnvFlagSet) null) // No-op + .setEnvFlags((EnvFlags) null) // No-op + .setEnvFlags((EnvFlags[]) null) // No-op + .setEnvFlags((Collection) null) // No-op + .setEnvFlags(MDB_NOSYNC) // Will be overwritten + .setEnvFlags(Arrays.asList(MDB_NOSUBDIR, MDB_NOTLS)) + .open(file)) { + env.sync(true); + assertThat(Files.isRegularFile(file)).isTrue(); + assertThat(env.getEnvFlagSet().getFlags()) + .containsExactlyInAnyOrderElementsOf(EnvFlagSet.of(MDB_NOSUBDIR, MDB_NOTLS).getFlags()); + } + } + + @Test + void setEnvFlags2() { + final Path dir = tempDir.createTempDir(); + try (Env env = + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .setEnvFlags(MDB_NOSUBDIR, MDB_NOTLS) + .setEnvFlags(Collections.emptySet()) // Clears them + .open(dir)) { + env.sync(true); + assertThat(env.getEnvFlagSet().getFlags()).isEmpty(); + assertThat(Files.isDirectory(dir)); + } + } + + @Test + void setEnvFlags_null1() { + final Path file = tempDir.createTempFile(); + // MDB_NOSUBDIR is cleared out so it will error as file is a file not a dir + Assertions.assertThatThrownBy( + () -> { + try (Env env = + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((Collection) null) // Clears the flags + .open(file)) {} + }) + .isInstanceOf(LmdbNativeException.class); + } + + @Test + void setEnvFlags_null2() { + final Path file = tempDir.createTempFile(); + // MDB_NOSUBDIR is cleared out so it will error as file is a file not a dir + Assertions.assertThatThrownBy( + () -> { + try (Env env = + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((EnvFlags) null) // Clears the flags + .open(file)) {} + }) + .isInstanceOf(LmdbNativeException.class); + } + + @Test + void setEnvFlags_null3() { + final Path file = tempDir.createTempFile(); + // MDB_NOSUBDIR is cleared out so it will error as file is a file not a dir + Assertions.assertThatThrownBy( + () -> { + try (Env env = + Env.create() + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxDbs(1) + .setMaxReaders(1) + .addEnvFlag(MDB_NOSUBDIR) + .setEnvFlags((EnvFlagSet) null) // Clears the flags + .open(file)) {} + }) + .isInstanceOf(LmdbNativeException.class); } } diff --git a/src/test/java/org/lmdbjava/GarbageCollectionTest.java b/src/test/java/org/lmdbjava/GarbageCollectionTest.java index f0aa64e4..4aa1245f 100644 --- a/src/test/java/org/lmdbjava/GarbageCollectionTest.java +++ b/src/test/java/org/lmdbjava/GarbageCollectionTest.java @@ -20,14 +20,14 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.fail; import static org.lmdbjava.DbiFlags.MDB_CREATE; -import static org.lmdbjava.Env.create; import java.nio.ByteBuffer; +import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import org.mockito.Mockito; -public class GarbageCollectionTest { +class GarbageCollectionTest { private static final String DB_NAME = "my DB"; private static final String KEY_PREFIX = "Uncorruptedkey"; @@ -35,61 +35,65 @@ public class GarbageCollectionTest { @Test void buffersNotGarbageCollectedTest() { - FileUtil.useTempDir( - dir -> { - try (Env env = - create().setMapSize(2_085_760_999).setMaxDbs(1).open(dir.toFile())) { - final Dbi db = env.openDbi(DB_NAME, MDB_CREATE); + try (final TempDir tempDir = new TempDir()) { + final Path dir = tempDir.createTempDir(); + try (Env env = Env.create().setMapSize(2_085_760_999).setMaxDbs(1).open(dir)) { + final Dbi db = + env.createDbi() + .setDbName(DB_NAME) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); - try (Txn txn = env.txnWrite()) { - for (int i = 0; i < 5_000; i++) { - putBuffer(db, txn, i); - } - txn.commit(); - } + try (Txn txn = env.txnWrite()) { + for (int i = 0; i < 5_000; i++) { + putBuffer(db, txn, i); + } + txn.commit(); + } - // Call GC before writing to LMDB and after last reference to buffer by - // changing the behavior of mask - try (MockedStatic mockedStatic = Mockito.mockStatic(MaskedFlag.class)) { - mockedStatic - .when(MaskedFlag::mask) - .thenAnswer( - invocationOnMock -> { - System.gc(); - return 0; - }); - final int gcRecordWrites = Integer.getInteger("gcRecordWrites", 50); - try (Txn txn = env.txnWrite()) { - for (int i = 0; i < gcRecordWrites; i++) { - putBuffer(db, txn, i); - } - txn.commit(); - } + // Call GC before writing to LMDB and after last reference to buffer by + // changing the behavior of mask + try (MockedStatic mockedStatic = Mockito.mockStatic(MaskedFlag.class)) { + mockedStatic + .when(MaskedFlag::mask) + .thenAnswer( + invocationOnMock -> { + System.gc(); + return 0; + }); + final int gcRecordWrites = Integer.getInteger("gcRecordWrites", 50); + try (Txn txn = env.txnWrite()) { + for (int i = 0; i < gcRecordWrites; i++) { + putBuffer(db, txn, i); } + txn.commit(); + } + } - // Find corrupt keys - try (Txn txn = env.txnRead()) { - try (Cursor c = db.openCursor(txn)) { - if (c.first()) { - do { - final byte[] rkey = new byte[c.key().remaining()]; - c.key().get(rkey); - final byte[] rval = new byte[c.val().remaining()]; - c.val().get(rval); - final String skey = new String(rkey, UTF_8); - final String sval = new String(rval, UTF_8); - if (!skey.startsWith("Uncorruptedkey")) { - fail("Found corrupt key " + skey); - } - if (!sval.startsWith("Uncorruptedval")) { - fail("Found corrupt val " + sval); - } - } while (c.next()); + // Find corrupt keys + try (Txn txn = env.txnRead()) { + try (Cursor c = db.openCursor(txn)) { + if (c.first()) { + do { + final byte[] rkey = new byte[c.key().remaining()]; + c.key().get(rkey); + final byte[] rval = new byte[c.val().remaining()]; + c.val().get(rval); + final String skey = new String(rkey, UTF_8); + final String sval = new String(rval, UTF_8); + if (!skey.startsWith("Uncorruptedkey")) { + fail("Found corrupt key " + skey); + } + if (!sval.startsWith("Uncorruptedval")) { + fail("Found corrupt val " + sval); } - } + } while (c.next()); } } - }); + } + } + } } private void putBuffer(final Dbi db, final Txn txn, final int i) { 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..e6a86a96 --- /dev/null +++ b/src/test/java/org/lmdbjava/PutFlagSetTest.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 static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class PutFlagSetTest extends AbstractFlagSetTest { + + @Test + void test() { + // This is here purely to stop CodeQL moaning that this class is unused. + // All the actual tests are in the superclass + Assertions.assertThat(getAllFlags()).isNotNull(); + } + + @Override + List getAllFlags() { + return Arrays.stream(PutFlags.values()).collect(Collectors.toList()); + } + + @Override + PutFlagSet getEmptyFlagSet() { + return PutFlagSet.empty(); + } + + @Override + AbstractFlagSet.Builder getBuilder() { + return PutFlagSet.builder(); + } + + @Override + PutFlagSet getFlagSet(Collection flags) { + return PutFlagSet.of(flags); + } + + @Override + PutFlagSet getFlagSet(PutFlags[] flags) { + return PutFlagSet.of(flags); + } + + @Override + PutFlagSet getFlagSet(PutFlags flag) { + return PutFlagSet.of(flag); + } + + @Override + Class getFlagType() { + return PutFlags.class; + } + + @Override + Function, PutFlagSet> getConstructor() { + return PutFlagSetImpl::new; + } + + /** + * {@link FlagSet#isSet(MaskedFlag)} on the flag enum is tested in {@link AbstractFlagSetTest} but + * the coverage check doesn't seem to notice it. + */ + @Test + void testIsSet() { + assertThat(PutFlags.MDB_APPEND.isSet(PutFlags.MDB_APPEND)).isTrue(); + assertThat(PutFlags.MDB_APPEND.isSet(PutFlags.MDB_MULTIPLE)).isFalse(); + //noinspection ConstantValue + assertThat(PutFlags.MDB_APPEND.isSet(null)).isFalse(); + } + + @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/TargetNameTest.java b/src/test/java/org/lmdbjava/TargetNameTest.java index 04caea83..6c0a8f44 100644 --- a/src/test/java/org/lmdbjava/TargetNameTest.java +++ b/src/test/java/org/lmdbjava/TargetNameTest.java @@ -41,9 +41,15 @@ void customEmbedded() { @Test void embeddedNameResolution() { - embed("aarch64-linux-gnu.so", "aarch64", "Linux"); + // Note: Linux resolution now detects musl vs glibc at runtime + // These tests verify the resolution logic but actual toolchain depends on system + final String linuxToolchain = + TargetName.resolveFilename(NONE, NONE, "x86_64", "Linux").contains("-musl.") + ? "musl" + : "gnu"; + embed("aarch64-linux-" + linuxToolchain + ".so", "aarch64", "Linux"); embed("aarch64-macos-none.so", "aarch64", "Mac OS"); - embed("x86_64-linux-gnu.so", "x86_64", "Linux"); + embed("x86_64-linux-" + linuxToolchain + ".so", "x86_64", "Linux"); embed("x86_64-macos-none.so", "x86_64", "Mac OS"); embed("x86_64-windows-gnu.dll", "x86_64", "Windows"); } diff --git a/src/test/java/org/lmdbjava/TempDir.java b/src/test/java/org/lmdbjava/TempDir.java new file mode 100644 index 00000000..d8acb2b9 --- /dev/null +++ b/src/test/java/org/lmdbjava/TempDir.java @@ -0,0 +1,57 @@ +/* + * 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.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.UUID; + +class TempDir implements AutoCloseable { + private final Path root; + + public TempDir() { + try { + root = Files.createTempDirectory("lmdb"); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + } + + public Path createTempFile() { + return root.resolve(UUID.randomUUID().toString()); + } + + public Path createTempDir() { + try { + final Path dir = root.resolve(UUID.randomUUID().toString()); + Files.createDirectory(dir); + return dir; + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + } + + public void cleanup() { + FileUtil.deleteDir(root); + } + + @Override + public void close() { + cleanup(); + } +} diff --git a/src/test/java/org/lmdbjava/TestUtils.java b/src/test/java/org/lmdbjava/TestUtils.java index c0203264..a15dc6b2 100644 --- a/src/test/java/org/lmdbjava/TestUtils.java +++ b/src/test/java/org/lmdbjava/TestUtils.java @@ -16,13 +16,18 @@ package org.lmdbjava; import static io.netty.buffer.PooledByteBufAllocator.DEFAULT; -import static java.lang.Integer.BYTES; import static java.nio.ByteBuffer.allocateDirect; import io.netty.buffer.ByteBuf; 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.Comparator; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; import org.agrona.MutableDirectBuffer; import org.agrona.concurrent.UnsafeBuffer; @@ -30,8 +35,9 @@ final class TestUtils { public static final String DB_1 = "test-db-1"; - - public static final int POSIX_MODE = 0664; + 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"; private TestUtils() {} @@ -46,11 +52,93 @@ static int fromBa(final byte[] ba) { } static ByteBuffer bb(final int value) { - final ByteBuffer bb = allocateDirect(BYTES); + final ByteBuffer bb = allocateDirect(Integer.BYTES); bb.putInt(value).flip(); return bb; } + static ByteBuffer bb(final long value) { + final ByteBuffer bb = allocateDirect(Long.BYTES); + bb.putLong(value).flip(); + 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() == Integer.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 byte[] getBytes(final ByteBuffer byteBuffer) { + if (byteBuffer == null) { + return null; + } + final byte[] bytes = new byte[byteBuffer.remaining()]; + byteBuffer.duplicate().get(bytes); + return bytes; + } + + static int parseInt(final String str) { + Objects.requireNonNull(str); + final String trimmed = str.trim(); + try { + return Integer.parseInt(trimmed); + } catch (NumberFormatException e) { + throw new RuntimeException("Unable to parse '" + trimmed + "' as an int."); + } + } + + static long parseLong(final String str) { + Objects.requireNonNull(str); + final String trimmed = str.trim(); + try { + return Long.parseLong(trimmed); + } catch (NumberFormatException e) { + throw new RuntimeException("Unable to parse '" + trimmed + "' as a long."); + } + } + static void invokePrivateConstructor(final Class clazz) { try { final Constructor c = clazz.getDeclaredConstructor(); @@ -66,14 +154,52 @@ static void invokePrivateConstructor(final Class clazz) { } static MutableDirectBuffer mdb(final int value) { - final MutableDirectBuffer b = new UnsafeBuffer(allocateDirect(BYTES)); + final MutableDirectBuffer b = new UnsafeBuffer(allocateDirect(Integer.BYTES)); b.putInt(0, value); return b; } static ByteBuf nb(final int value) { - final ByteBuf b = DEFAULT.directBuffer(BYTES); + final ByteBuf b = DEFAULT.directBuffer(Integer.BYTES); 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); + } + } + + static ComparatorResult compare(final Comparator comparator, final T o1, final T o2) { + Objects.requireNonNull(comparator); + final int result = comparator.compare(o1, o2); + return ComparatorResult.get(result); + } } diff --git a/src/test/java/org/lmdbjava/TutorialTest.java b/src/test/java/org/lmdbjava/TutorialTest.java index 1b42b327..c2271363 100644 --- a/src/test/java/org/lmdbjava/TutorialTest.java +++ b/src/test/java/org/lmdbjava/TutorialTest.java @@ -16,27 +16,32 @@ package org.lmdbjava; -import static java.nio.ByteBuffer.allocateDirect; import static java.nio.charset.StandardCharsets.UTF_8; -import static java.util.concurrent.Executors.newCachedThreadPool; import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; import static org.lmdbjava.ByteBufferProxy.PROXY_OPTIMAL; 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.DirectBufferProxy.PROXY_DB; -import static org.lmdbjava.Env.create; import static org.lmdbjava.GetOp.MDB_SET; import static org.lmdbjava.SeekOp.MDB_FIRST; import static org.lmdbjava.SeekOp.MDB_LAST; import static org.lmdbjava.SeekOp.MDB_PREV; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.agrona.DirectBuffer; import org.agrona.MutableDirectBuffer; import org.agrona.concurrent.UnsafeBuffer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.lmdbjava.CursorIterable.KeyVal; @@ -57,147 +62,157 @@ public final class TutorialTest { private static final String DB_NAME = "my DB"; + private TempDir tempDir; + + @BeforeEach + void beforeEach() { + tempDir = new TempDir(); + } + + @AfterEach + void afterEach() { + tempDir.cleanup(); + } + /** In this first tutorial we will use LmdbJava with some basic defaults. */ @Test void tutorial1() { // We need a storage directory first. // The path cannot be on a remote file system. - FileUtil.useTempDir( - dir -> { - - // We always need an Env. An Env owns a physical on-disk storage file. One - // Env can store many different databases (ie sorted maps). - final Env env = - create() - // LMDB also needs to know how large our DB might be. Over-estimating is OK. - .setMapSize(10_485_760) - // LMDB also needs to know how many DBs (Dbi) we want to store in this Env. - .setMaxDbs(1) - // Now let's open the Env. The same path can be concurrently opened and - // used in different processes, but do not open the same path twice in - // the same process at the same time. - .open(dir.toFile()); - - // We need a Dbi for each DB. A Dbi roughly equates to a sorted map. The - // MDB_CREATE flag causes the DB to be created if it doesn't already exist. - final Dbi db = env.openDbi(DB_NAME, MDB_CREATE); - - // We want to store some data, so we will need a direct ByteBuffer. - // Note that LMDB keys cannot exceed maxKeySize bytes (511 bytes by default). - // Values can be larger. - final ByteBuffer key = allocateDirect(env.getMaxKeySize()); - final ByteBuffer val = allocateDirect(700); - key.put("greeting".getBytes(UTF_8)).flip(); - val.put("Hello world".getBytes(UTF_8)).flip(); - final int valSize = val.remaining(); - - // Now store it. Dbi.put() internally begins and commits a transaction (Txn). - db.put(key, val); - - // To fetch any data from LMDB we need a Txn. A Txn is very important in - // LmdbJava because it offers ACID characteristics and internally holds a - // read-only key buffer and read-only value buffer. These read-only buffers - // are always the same two Java objects, but point to different LMDB-managed - // memory as we use Dbi (and Cursor) methods. These read-only buffers remain - // valid only until the Txn is released or the next Dbi or Cursor call. If - // you need data afterwards, you should copy the bytes to your own buffer. - try (Txn txn = env.txnRead()) { - final ByteBuffer found = db.get(txn, key); - assertThat(found).isNotNull(); - - // The fetchedVal is read-only and points to LMDB memory - final ByteBuffer fetchedVal = txn.val(); - assertThat(fetchedVal.remaining()).isEqualTo(valSize); - - // Let's double-check the fetched value is correct - assertThat(UTF_8.decode(fetchedVal).toString()).isEqualTo("Hello world"); - } - - // We can also delete. The simplest way is to let Dbi allocate a new Txn... - db.delete(key); - - // Now if we try to fetch the deleted row, it won't be present - try (Txn txn = env.txnRead()) { - assertThat(db.get(txn, key)).isNull(); - } - - env.close(); - }); + final Path dir = tempDir.createTempDir(); + + // We always need an Env. An Env owns a physical on-disk storage file. One + // Env can store many different databases (ie sorted maps). + final Env env = + Env.create() + // LMDB also needs to know how large our DB might be. Over-estimating is OK. + .setMapSize(10_485_760) + // LMDB also needs to know how many DBs (Dbi) we want to store in this Env. + .setMaxDbs(1) + // Now let's open the Env. The same path can be concurrently opened and + // used in different processes, but do not open the same path twice in + // the same process at the same time. + .open(dir); + + // We need a Dbi for each DB. A Dbi roughly equates to a sorted map. The + // MDB_CREATE flag causes the DB to be created if it doesn't already exist. + final Dbi db = + env.createDbi().setDbName(DB_NAME).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + + // We want to store some data, so we will need a direct ByteBuffer. + // Note that LMDB keys cannot exceed maxKeySize bytes (511 bytes by default). + // Values can be larger. + final ByteBuffer key = ByteBuffer.allocateDirect(env.getMaxKeySize()); + final ByteBuffer val = ByteBuffer.allocateDirect(700); + key.put("greeting".getBytes(UTF_8)).flip(); + val.put("Hello world".getBytes(UTF_8)).flip(); + final int valSize = val.remaining(); + + // Now store it. Dbi.put() internally begins and commits a transaction (Txn). + db.put(key, val); + + // To fetch any data from LMDB we need a Txn. A Txn is very important in + // LmdbJava because it offers ACID characteristics and internally holds a + // read-only key buffer and read-only value buffer. These read-only buffers + // are always the same two Java objects, but point to different LMDB-managed + // memory as we use Dbi (and Cursor) methods. These read-only buffers remain + // valid only until the Txn is released or the next Dbi or Cursor call. If + // you need data afterwards, you should copy the bytes to your own buffer. + try (Txn txn = env.txnRead()) { + final ByteBuffer found = db.get(txn, key); + assertThat(found).isNotNull(); + + // The fetchedVal is read-only and points to LMDB memory + final ByteBuffer fetchedVal = txn.val(); + assertThat(fetchedVal.remaining()).isEqualTo(valSize); + + // Let's double-check the fetched value is correct + assertThat(UTF_8.decode(fetchedVal).toString()).isEqualTo("Hello world"); + } + + // We can also delete. The simplest way is to let Dbi allocate a new Txn... + db.delete(key); + + // Now if we try to fetch the deleted row, it won't be present + try (Txn txn = env.txnRead()) { + assertThat(db.get(txn, key)).isNull(); + } + + env.close(); } /** In this second tutorial we'll learn more about LMDB's ACID Txns. */ @Test void tutorial2() { - FileUtil.useTempDir( - dir -> { - try { - final Env env = createSimpleEnv(dir); - final Dbi db = env.openDbi(DB_NAME, MDB_CREATE); - final ByteBuffer key = allocateDirect(env.getMaxKeySize()); - final ByteBuffer val = allocateDirect(700); - - // Let's write and commit "key1" via a Txn. A Txn can include multiple Dbis. - // Note write Txns block other write Txns, due to writes being serialized. - // It's therefore important to avoid unnecessarily long-lived write Txns. + final Path dir = tempDir.createTempDir(); + try { + final Env env = createSimpleEnv(dir); + final Dbi db = + env.createDbi().setDbName(DB_NAME).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + final ByteBuffer key = ByteBuffer.allocateDirect(env.getMaxKeySize()); + final ByteBuffer val = ByteBuffer.allocateDirect(700); + + // Let's write and commit "key1" via a Txn. A Txn can include multiple Dbis. + // Note write Txns block other write Txns, due to writes being serialized. + // It's therefore important to avoid unnecessarily long-lived write Txns. + try (Txn txn = env.txnWrite()) { + key.put("key1".getBytes(UTF_8)).flip(); + val.put("lmdb".getBytes(UTF_8)).flip(); + db.put(txn, key, val); + + // We can read data too, even though this is a write Txn. + final ByteBuffer found = db.get(txn, key); + assertThat(found).isNotNull(); + + // An explicit commit is required, otherwise Txn.close() rolls it back. + txn.commit(); + } + + // Open a read-only Txn. It only sees data that existed at Txn creation time. + final Txn rtx = env.txnRead(); + + // Our read Txn can fetch key1 without problem, as it existed at Txn creation. + ByteBuffer found = db.get(rtx, key); + assertThat(found).isNotNull(); + + // Note that our main test thread holds the Txn. Only one Txn per thread is + // typically permitted (the exception is a read-only Env with MDB_NOTLS). + // + // Let's write out a "key2" via a new write Txn in a different thread. + final ExecutorService es = Executors.newCachedThreadPool(); + es.execute( + () -> { try (Txn txn = env.txnWrite()) { - key.put("key1".getBytes(UTF_8)).flip(); - val.put("lmdb".getBytes(UTF_8)).flip(); + key.clear(); + key.put("key2".getBytes(UTF_8)).flip(); db.put(txn, key, val); - - // We can read data too, even though this is a write Txn. - final ByteBuffer found = db.get(txn, key); - assertThat(found).isNotNull(); - - // An explicit commit is required, otherwise Txn.close() rolls it back. txn.commit(); } - - // Open a read-only Txn. It only sees data that existed at Txn creation time. - final Txn rtx = env.txnRead(); - - // Our read Txn can fetch key1 without problem, as it existed at Txn creation. - ByteBuffer found = db.get(rtx, key); - assertThat(found).isNotNull(); - - // Note that our main test thread holds the Txn. Only one Txn per thread is - // typically permitted (the exception is a read-only Env with MDB_NOTLS). - // - // Let's write out a "key2" via a new write Txn in a different thread. - final ExecutorService es = newCachedThreadPool(); - es.execute( - () -> { - try (Txn txn = env.txnWrite()) { - key.clear(); - key.put("key2".getBytes(UTF_8)).flip(); - db.put(txn, key, val); - txn.commit(); - } - }); - es.shutdown(); - es.awaitTermination(10, SECONDS); - - // Even though key2 has been committed, our read Txn still can't see it. - found = db.get(rtx, key); - assertThat(found).isNull(); - - // To see key2, we could create a new Txn. But a reset/renew is much faster. - // Reset/renew is also important to avoid long-lived read Txns, as these - // prevent the re-use of free pages by write Txns (ie the DB will grow). - rtx.reset(); - // ... potentially long operation here ... - rtx.renew(); - found = db.get(rtx, key); - assertThat(found).isNotNull(); - - // Don't forget to close the read Txn now we're completely finished. We could - // have avoided this if we used a try-with-resources block, but we wanted to - // play around with multiple concurrent Txns to demonstrate the "I" in ACID. - rtx.close(); - env.close(); - } catch (final InterruptedException e) { - throw new RuntimeException(e); - } - }); + }); + es.shutdown(); + es.awaitTermination(10, SECONDS); + + // Even though key2 has been committed, our read Txn still can't see it. + found = db.get(rtx, key); + assertThat(found).isNull(); + + // To see key2, we could create a new Txn. But a reset/renew is much faster. + // Reset/renew is also important to avoid long-lived read Txns, as these + // prevent the re-use of free pages by write Txns (ie the DB will grow). + rtx.reset(); + // ... potentially long operation here ... + rtx.renew(); + found = db.get(rtx, key); + assertThat(found).isNotNull(); + + // Don't forget to close the read Txn now we're completely finished. We could + // have avoided this if we used a try-with-resources block, but we wanted to + // play around with multiple concurrent Txns to demonstrate the "I" in ACID. + rtx.close(); + env.close(); + } catch (final InterruptedException e) { + throw new RuntimeException(e); + } } /** @@ -207,73 +222,72 @@ void tutorial2() { */ @Test void tutorial3() { - FileUtil.useTempDir( - dir -> { - final Env env = createSimpleEnv(dir); - final Dbi db = env.openDbi(DB_NAME, MDB_CREATE); - final ByteBuffer key = allocateDirect(env.getMaxKeySize()); - final ByteBuffer val = allocateDirect(700); - - try (Txn txn = env.txnWrite()) { - // A cursor always belongs to a particular Dbi. - final Cursor c = db.openCursor(txn); - - // We can put via a Cursor. Note we're adding keys in a strange order, - // as we want to show you that LMDB returns them in sorted order. - key.put("zzz".getBytes(UTF_8)).flip(); - val.put("lmdb".getBytes(UTF_8)).flip(); - c.put(key, val); - key.clear(); - key.put("aaa".getBytes(UTF_8)).flip(); - c.put(key, val); - key.clear(); - key.put("ccc".getBytes(UTF_8)).flip(); - c.put(key, val); - - // We can read from the Cursor by key. - c.get(key, MDB_SET); - assertThat(UTF_8.decode(c.key()).toString()).isEqualTo("ccc"); - - // Let's see that LMDB provides the keys in appropriate order.... - c.seek(MDB_FIRST); - assertThat(UTF_8.decode(c.key()).toString()).isEqualTo("aaa"); - - c.seek(MDB_LAST); - assertThat(UTF_8.decode(c.key()).toString()).isEqualTo("zzz"); - - c.seek(MDB_PREV); - assertThat(UTF_8.decode(c.key()).toString()).isEqualTo("ccc"); - - // Cursors can also delete the current key. - c.delete(); - - c.close(); - txn.commit(); - } - - // A read-only Cursor can survive its original Txn being closed. This is - // useful if you want to close the original Txn (eg maybe you created the - // Cursor during the constructor of a singleton with a throw-away Txn). Of - // course, you cannot use the Cursor if its Txn is closed or currently reset. - final Txn tx1 = env.txnRead(); - final Cursor c = db.openCursor(tx1); - tx1.close(); - - // The Cursor becomes usable again by "renewing" it with an active read Txn. - final Txn tx2 = env.txnRead(); - c.renew(tx2); - c.seek(MDB_FIRST); - - // As usual with read Txns, we can reset and renew them. The Cursor does - // not need any special handling if we do this. - tx2.reset(); - // ... potentially long operation here ... - tx2.renew(); - c.seek(MDB_LAST); - - tx2.close(); - env.close(); - }); + final Path dir = tempDir.createTempDir(); + final Env env = createSimpleEnv(dir); + final Dbi db = + env.createDbi().setDbName(DB_NAME).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + final ByteBuffer key = ByteBuffer.allocateDirect(env.getMaxKeySize()); + final ByteBuffer val = ByteBuffer.allocateDirect(700); + + try (Txn txn = env.txnWrite()) { + // A cursor always belongs to a particular Dbi. + final Cursor c = db.openCursor(txn); + + // We can put via a Cursor. Note we're adding keys in a strange order, + // as we want to show you that LMDB returns them in sorted order. + key.put("zzz".getBytes(UTF_8)).flip(); + val.put("lmdb".getBytes(UTF_8)).flip(); + c.put(key, val); + key.clear(); + key.put("aaa".getBytes(UTF_8)).flip(); + c.put(key, val); + key.clear(); + key.put("ccc".getBytes(UTF_8)).flip(); + c.put(key, val); + + // We can read from the Cursor by key. + c.get(key, MDB_SET); + assertThat(UTF_8.decode(c.key()).toString()).isEqualTo("ccc"); + + // Let's see that LMDB provides the keys in appropriate order.... + c.seek(MDB_FIRST); + assertThat(UTF_8.decode(c.key()).toString()).isEqualTo("aaa"); + + c.seek(MDB_LAST); + assertThat(UTF_8.decode(c.key()).toString()).isEqualTo("zzz"); + + c.seek(MDB_PREV); + assertThat(UTF_8.decode(c.key()).toString()).isEqualTo("ccc"); + + // Cursors can also delete the current key. + c.delete(); + + c.close(); + txn.commit(); + } + + // A read-only Cursor can survive its original Txn being closed. This is + // useful if you want to close the original Txn (eg maybe you created the + // Cursor during the constructor of a singleton with a throw-away Txn). Of + // course, you cannot use the Cursor if its Txn is closed or currently reset. + final Txn tx1 = env.txnRead(); + final Cursor c = db.openCursor(tx1); + tx1.close(); + + // The Cursor becomes usable again by "renewing" it with an active read Txn. + final Txn tx2 = env.txnRead(); + c.renew(tx2); + c.seek(MDB_FIRST); + + // As usual with read Txns, we can reset and renew them. The Cursor does + // not need any special handling if we do this. + tx2.reset(); + // ... potentially long operation here ... + tx2.renew(); + c.seek(MDB_LAST); + + tx2.close(); + env.close(); } /** @@ -282,109 +296,111 @@ void tutorial3() { */ @Test void tutorial4() { - FileUtil.useTempDir( - dir -> { - final Env env = createSimpleEnv(dir); - final Dbi db = env.openDbi(DB_NAME, MDB_CREATE); - - try (Txn txn = env.txnWrite()) { - final ByteBuffer key = allocateDirect(env.getMaxKeySize()); - final ByteBuffer val = allocateDirect(700); - - // Insert some data. Note that ByteBuffer order defaults to Big Endian. - // LMDB does not persist the byte order, but it's critical to sort keys. - // If your numeric keys don't sort as expected, review buffer byte order. - val.putInt(100); - key.putInt(1); - db.put(txn, key, val); - key.clear(); - key.putInt(2); - db.put(txn, key, val); - key.clear(); - - // Each iterable uses a cursor and must be closed when finished. Iterate - // forward in terms of key ordering starting with the first key. - try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { - for (final KeyVal kv : ci) { - assertThat(kv.key()).isNotNull(); - assertThat(kv.val()).isNotNull(); - } - } - - // Iterate backward in terms of key ordering starting with the last key. - try (CursorIterable ci = db.iterate(txn, KeyRange.allBackward())) { - for (final KeyVal kv : ci) { - assertThat(kv.key()).isNotNull(); - assertThat(kv.val()).isNotNull(); - } - } - - // There are many ways to control the desired key range via KeyRange, such - // as arbitrary start and stop values, direction etc. We've adopted Guava's - // terminology for our range classes (see KeyRangeType for further details). - key.putInt(1); - final KeyRange range = KeyRange.atLeastBackward(key); - try (CursorIterable ci = db.iterate(txn, range)) { - for (final KeyVal kv : ci) { - assertThat(kv.key()).isNotNull(); - assertThat(kv.val()).isNotNull(); - } - } - } - - env.close(); - }); + final Path dir = tempDir.createTempDir(); + final Env env = createSimpleEnv(dir); + final Dbi db = + env.createDbi().setDbName(DB_NAME).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + + try (Txn txn = env.txnWrite()) { + final ByteBuffer key = ByteBuffer.allocateDirect(env.getMaxKeySize()); + final ByteBuffer val = ByteBuffer.allocateDirect(700); + + // Insert some data. Note that ByteBuffer order defaults to Big Endian. + // LMDB does not persist the byte order, but it's critical to sort keys. + // If your numeric keys don't sort as expected, review buffer byte order. + val.putInt(100); + key.putInt(1); + db.put(txn, key, val); + key.clear(); + key.putInt(2); + db.put(txn, key, val); + key.clear(); + + // Each iterable uses a cursor and must be closed when finished. Iterate + // forward in terms of key ordering starting with the first key. + try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { + for (final KeyVal kv : ci) { + assertThat(kv.key()).isNotNull(); + assertThat(kv.val()).isNotNull(); + } + } + + // Iterate backward in terms of key ordering starting with the last key. + try (CursorIterable ci = db.iterate(txn, KeyRange.allBackward())) { + for (final KeyVal kv : ci) { + assertThat(kv.key()).isNotNull(); + assertThat(kv.val()).isNotNull(); + } + } + + // There are many ways to control the desired key range via KeyRange, such + // as arbitrary start and stop values, direction etc. We've adopted Guava's + // terminology for our range classes (see KeyRangeType for further details). + key.putInt(1); + final KeyRange range = KeyRange.atLeastBackward(key); + try (CursorIterable ci = db.iterate(txn, range)) { + for (final KeyVal kv : ci) { + assertThat(kv.key()).isNotNull(); + assertThat(kv.val()).isNotNull(); + } + } + } + + env.close(); } /** In this fifth tutorial we'll explore multiple values sharing a single key. */ @Test void tutorial5() { - FileUtil.useTempDir( - dir -> { - final Env env = createSimpleEnv(dir); - - // This time we're going to tell the Dbi it can store > 1 value per key. - // There are other flags available if we're storing integers etc. - final Dbi db = env.openDbi(DB_NAME, MDB_CREATE, MDB_DUPSORT); - - // Duplicate support requires both keys and values to be <= max key size. - final ByteBuffer key = allocateDirect(env.getMaxKeySize()); - final ByteBuffer val = allocateDirect(env.getMaxKeySize()); - - try (Txn txn = env.txnWrite()) { - final Cursor c = db.openCursor(txn); - - // Store one key, but many values, and in non-natural order. - key.put("key".getBytes(UTF_8)).flip(); - val.put("xxx".getBytes(UTF_8)).flip(); - c.put(key, val); - val.clear(); - val.put("kkk".getBytes(UTF_8)).flip(); - c.put(key, val); - val.clear(); - val.put("lll".getBytes(UTF_8)).flip(); - c.put(key, val); - - // Cursor can tell us how many values the current key has. - final long count = c.count(); - assertThat(count).isEqualTo(3L); - - // Let's position the Cursor. Note sorting still works. - c.seek(MDB_FIRST); - assertThat(UTF_8.decode(c.val()).toString()).isEqualTo("kkk"); - - c.seek(MDB_LAST); - assertThat(UTF_8.decode(c.val()).toString()).isEqualTo("xxx"); - - c.seek(MDB_PREV); - assertThat(UTF_8.decode(c.val()).toString()).isEqualTo("lll"); - - c.close(); - txn.commit(); - } - - env.close(); - }); + final Path dir = tempDir.createTempDir(); + final Env env = createSimpleEnv(dir); + + // This time we're going to tell the Dbi it can store > 1 value per key. + // There are other flags available if we're storing integers etc. + final Dbi db = + env.createDbi() + .setDbName(DB_NAME) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE, MDB_DUPSORT) + .open(); + + // Duplicate support requires both keys and values to be <= max key size. + final ByteBuffer key = ByteBuffer.allocateDirect(env.getMaxKeySize()); + final ByteBuffer val = ByteBuffer.allocateDirect(env.getMaxKeySize()); + + try (Txn txn = env.txnWrite()) { + final Cursor c = db.openCursor(txn); + + // Store one key, but many values, and in non-natural order. + key.put("key".getBytes(UTF_8)).flip(); + val.put("xxx".getBytes(UTF_8)).flip(); + c.put(key, val); + val.clear(); + val.put("kkk".getBytes(UTF_8)).flip(); + c.put(key, val); + val.clear(); + val.put("lll".getBytes(UTF_8)).flip(); + c.put(key, val); + + // Cursor can tell us how many values the current key has. + final long count = c.count(); + assertThat(count).isEqualTo(3L); + + // Let's position the Cursor. Note sorting still works. + c.seek(MDB_FIRST); + assertThat(UTF_8.decode(c.val()).toString()).isEqualTo("kkk"); + + c.seek(MDB_LAST); + assertThat(UTF_8.decode(c.val()).toString()).isEqualTo("xxx"); + + c.seek(MDB_PREV); + assertThat(UTF_8.decode(c.val()).toString()).isEqualTo("lll"); + + c.close(); + txn.commit(); + } + + env.close(); } /** @@ -393,85 +409,206 @@ void tutorial5() { */ @Test void tutorial6() { - FileUtil.useTempDir( - dir -> { - // Note we need to specify the Verifier's DBI_COUNT for the Env. - final Env env = - create(PROXY_OPTIMAL) - .setMapSize(10_485_760) - .setMaxDbs(Verifier.DBI_COUNT) - .open(dir.toFile()); - - // Create a Verifier (it's a Callable for those needing full control). - final Verifier v = new Verifier(env); - - // We now run the verifier for 3 seconds; it raises an exception on failure. - // The method returns the number of entries it successfully verified. - v.runFor(3, SECONDS); - - env.close(); - }); + final Path dir = tempDir.createTempDir(); + // Note we need to specify the Verifier's DBI_COUNT for the Env. + final Env env = + Env.create(PROXY_OPTIMAL) + .setMapSize(10, ByteUnit.MEBIBYTES) + .setMaxDbs(Verifier.DBI_COUNT) + .open(dir); + + // Create a Verifier (it's a Callable for those needing full control). + final Verifier v = new Verifier(env); + + // We now run the verifier for 3 seconds; it raises an exception on failure. + // The method returns the number of entries it successfully verified. + v.runFor(3, SECONDS); + + env.close(); } /** In this final tutorial we'll look at using Agrona's DirectBuffer. */ @Test void tutorial7() { - FileUtil.useTempDir( - dir -> { - // The critical difference is we pass the PROXY_DB field to Env.create(). - // There's also a PROXY_SAFE if you want to stop ByteBuffer's Unsafe use. - // Aside from that and a different type argument, it's the same as usual... - final Env env = - create(PROXY_DB).setMapSize(10_485_760).setMaxDbs(1).open(dir.toFile()); - - final Dbi db = env.openDbi(DB_NAME, MDB_CREATE); - - final ByteBuffer keyBb = allocateDirect(env.getMaxKeySize()); - final MutableDirectBuffer key = new UnsafeBuffer(keyBb); - final MutableDirectBuffer val = new UnsafeBuffer(allocateDirect(700)); - - try (Txn txn = env.txnWrite()) { - try (Cursor c = db.openCursor(txn)) { - // Agrona is faster than ByteBuffer and its methods are nicer... - val.putStringWithoutLengthUtf8(0, "The Value"); - key.putStringWithoutLengthUtf8(0, "yyy"); - c.put(key, val); - - key.putStringWithoutLengthUtf8(0, "ggg"); - c.put(key, val); - - c.seek(MDB_FIRST); - assertThat(c.key().getStringWithoutLengthUtf8(0, env.getMaxKeySize())) - .startsWith("ggg"); - - c.seek(MDB_LAST); - assertThat(c.key().getStringWithoutLengthUtf8(0, env.getMaxKeySize())) - .startsWith("yyy"); - - // DirectBuffer has no position concept. Often you don't want to store - // the unnecessary bytes of a varying-size buffer. Let's have a look... - final int keyLen = key.putStringWithoutLengthUtf8(0, "12characters"); - assertThat(keyLen).isEqualTo(12); - assertThat(key.capacity()).isEqualTo(env.getMaxKeySize()); - - // To only store the 12 characters, we simply call wrap: - key.wrap(key, 0, keyLen); - assertThat(key.capacity()).isEqualTo(keyLen); - c.put(key, val); - c.seek(MDB_FIRST); - assertThat(c.key().capacity()).isEqualTo(keyLen); - assertThat(c.key().getStringWithoutLengthUtf8(0, c.key().capacity())) - .isEqualTo("12characters"); - - // To store bigger values again, just wrap the original buffer. - key.wrap(keyBb); - assertThat(key.capacity()).isEqualTo(env.getMaxKeySize()); - } - txn.commit(); - } + final Path dir = tempDir.createTempDir(); + // The critical difference is we pass the PROXY_DB field to Env.create(). + // There's also a PROXY_SAFE if you want to stop ByteBuffer's Unsafe use. + // Aside from that and a different type argument, it's the same as usual... + final Env env = + Env.create(PROXY_DB).setMapSize(10, ByteUnit.MEBIBYTES).setMaxDbs(1).open(dir); + + final Dbi db = + env.createDbi().setDbName(DB_NAME).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + + final ByteBuffer keyBb = ByteBuffer.allocateDirect(env.getMaxKeySize()); + final MutableDirectBuffer key = new UnsafeBuffer(keyBb); + final MutableDirectBuffer val = new UnsafeBuffer(ByteBuffer.allocateDirect(700)); + + try (Txn txn = env.txnWrite()) { + try (Cursor c = db.openCursor(txn)) { + // Agrona is faster than ByteBuffer and its methods are nicer... + val.putStringWithoutLengthUtf8(0, "The Value"); + key.putStringWithoutLengthUtf8(0, "yyy"); + c.put(key, val); + + key.putStringWithoutLengthUtf8(0, "ggg"); + c.put(key, val); + + c.seek(MDB_FIRST); + assertThat(c.key().getStringWithoutLengthUtf8(0, env.getMaxKeySize())).startsWith("ggg"); + + c.seek(MDB_LAST); + assertThat(c.key().getStringWithoutLengthUtf8(0, env.getMaxKeySize())).startsWith("yyy"); + + // DirectBuffer has no position concept. Often you don't want to store + // the unnecessary bytes of a varying-size buffer. Let's have a look... + final int keyLen = key.putStringWithoutLengthUtf8(0, "12characters"); + assertThat(keyLen).isEqualTo(12); + assertThat(key.capacity()).isEqualTo(env.getMaxKeySize()); + + // To only store the 12 characters, we simply call wrap: + key.wrap(key, 0, keyLen); + assertThat(key.capacity()).isEqualTo(keyLen); + c.put(key, val); + c.seek(MDB_FIRST); + assertThat(c.key().capacity()).isEqualTo(keyLen); + assertThat(c.key().getStringWithoutLengthUtf8(0, c.key().capacity())) + .isEqualTo("12characters"); + + // To store bigger values again, just wrap the original buffer. + key.wrap(keyBb); + assertThat(key.capacity()).isEqualTo(env.getMaxKeySize()); + } + txn.commit(); + } + + env.close(); + } - env.close(); - }); + /** + * In this tutorial we'll look at using keys that are longs. The same approach applies would apply + * to int keys. + */ + @Test + void tutorial8() { + final Path dir = tempDir.createTempDir(); + final Env env = createSimpleEnv(dir); + + // This time we're going to tell the Dbi that all the keys are integers. + // MDB_INTEGERKEY applies to both int and long keys. + // LMDB can make optimisations for better performance. + final Dbi db = + env.createDbi() + .setDbName(DB_NAME) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE, MDB_INTEGERKEY) + .open(); + + // MDB_INTEGERKEY requires that the keys are written/read in native order + final ByteBuffer key = ByteBuffer.allocateDirect(Long.BYTES).order(ByteOrder.nativeOrder()); + final ByteBuffer val = ByteBuffer.allocateDirect(100); + + try (Txn txn = env.txnWrite()) { + + // Store one key, but many values, and in non-natural order. + key.putLong(42L).flip(); + val.put("val-42".getBytes(UTF_8)).flip(); + db.put(txn, key, val); + + key.clear(); + val.clear(); + key.putLong(1L).flip(); + val.put("val-1".getBytes(UTF_8)).flip(); + db.put(txn, key, val); + + key.clear(); + val.clear(); + key.putLong(Long.MAX_VALUE).flip(); + val.put(("val-" + Long.MAX_VALUE).getBytes(UTF_8)).flip(); + db.put(txn, key, val); + + key.clear(); + val.clear(); + key.putLong(1_000L).flip(); + val.put("val-1".getBytes(UTF_8)).flip(); + db.put(txn, key, val); + + // Get all the keys + final List keys = new ArrayList<>(); + try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { + for (final KeyVal kv : ci) { + assertThat(kv.key()).isNotNull(); + assertThat(kv.val()).isNotNull(); + keys.add(kv.key().order(ByteOrder.nativeOrder()).getLong()); + } + } + + assertThat(keys).containsExactly(1L, 42L, 1_000L, Long.MAX_VALUE); + + txn.commit(); + } + env.close(); + } + + /** In this tutorial we'll look storing the data in reverse order */ + @Test + void tutorial9() { + final Path dir = tempDir.createTempDir(); + final Env env = createSimpleEnv(dir); + + final Dbi db = + env.createDbi() + .setDbName(DB_NAME) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE, MDB_REVERSEKEY) + .open(); + + final ByteBuffer key = ByteBuffer.allocateDirect(100); + final ByteBuffer val = ByteBuffer.allocateDirect(100); + + try (Txn txn = env.txnWrite()) { + + // Store one key, but many values, and in non-natural order. + key.put("tac".getBytes(UTF_8)).flip(); + val.put("CAT".getBytes(UTF_8)).flip(); + db.put(txn, key, val); + + key.clear(); + val.clear(); + key.put("god".getBytes(UTF_8)).flip(); + val.put("DOG".getBytes(UTF_8)).flip(); + db.put(txn, key, val); + + key.clear(); + val.clear(); + key.put("esroh".getBytes(UTF_8)).flip(); + val.put("HORSE".getBytes(UTF_8)).flip(); + db.put(txn, key, val); + + key.clear(); + val.clear(); + key.put("tnahpele".getBytes(UTF_8)).flip(); + val.put("ELEPHANT".getBytes(UTF_8)).flip(); + db.put(txn, key, val); + + // Get all the keys + final List keys = new ArrayList<>(); + final List values = new ArrayList<>(); + try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { + for (final KeyVal kv : ci) { + assertThat(kv.key()).isNotNull(); + assertThat(kv.val()).isNotNull(); + keys.add(UTF_8.decode(kv.key()).toString()); + values.add(UTF_8.decode(kv.val()).toString()); + } + } + + assertThat(keys).containsExactly("tac", "god", "tnahpele", "esroh"); + assertThat(values).containsExactly("CAT", "DOG", "ELEPHANT", "HORSE"); + + txn.commit(); + } + env.close(); } // You've finished! There are lots of other neat things we could show you (eg @@ -479,6 +616,6 @@ void tutorial7() { // or reverse ordered keys, using Env.DISABLE_CHECKS_PROP etc), but you now // know enough to tackle the JavaDocs with confidence. Have fun! private Env createSimpleEnv(final Path path) { - return create().setMapSize(10_485_760).setMaxDbs(1).setMaxReaders(1).open(path.toFile()); + return Env.create().setMapSize(10, ByteUnit.MEBIBYTES).setMaxDbs(1).setMaxReaders(1).open(path); } } diff --git a/src/test/java/org/lmdbjava/TxnDeprecatedTest.java b/src/test/java/org/lmdbjava/TxnDeprecatedTest.java new file mode 100644 index 00000000..387e9fef --- /dev/null +++ b/src/test/java/org/lmdbjava/TxnDeprecatedTest.java @@ -0,0 +1,119 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.lmdbjava.Env.create; +import static org.lmdbjava.EnvFlags.MDB_NOSUBDIR; +import static org.lmdbjava.TxnFlags.MDB_RDONLY_TXN; + +import java.nio.ByteBuffer; +import java.nio.file.Path; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.lmdbjava.Env.AlreadyClosedException; +import org.lmdbjava.Txn.IncompatibleParent; + +/** + * Tests all the deprecated txn related methods in {@link Env}. Essentially a duplicate of {@link + * TxnTest}. When all the deprecated methods are deleted we can delete this test class. + * + * @deprecated Tests all the deprecated txn related methods in {@link Env}. + */ +@Deprecated +public final class TxnDeprecatedTest { + + private Path file; + private Env env; + + private TempDir tempDir; + + @BeforeEach + void beforeEach() { + tempDir = new TempDir(); + file = tempDir.createTempFile(); + env = + create() + .setMapSize(256, ByteUnit.KIBIBYTES) + .setMaxReaders(1) + .setMaxDbs(2) + .setEnvFlags(MDB_NOSUBDIR) + .open(file); + } + + @AfterEach + void afterEach() { + env.close(); + tempDir.cleanup(); + } + + @Test + public void txParent() { + try (Txn txRoot = env.txnWrite(); + Txn txChild = env.txn(txRoot, new TxnFlags[0])) { + assertThat(txRoot.getParent()).isNull(); + assertThat(txChild.getParent()).isEqualTo(txRoot); + } + } + + @Test + public void txParent2() { + try (Txn txRoot = env.txnWrite(); + Txn txChild = env.txn(txRoot, (TxnFlags[]) null)) { + assertThat(txRoot.getParent()).isNull(); + assertThat(txChild.getParent()).isEqualTo(txRoot); + } + } + + @Test + void txParentDeniedIfEnvClosed() { + assertThatThrownBy( + () -> { + try (Txn txRoot = env.txnWrite(); + Txn txChild = env.txn(txRoot, new TxnFlags[0])) { + env.close(); + assertThat(txChild.getParent()).isEqualTo(txRoot); + } + }) + .isInstanceOf(AlreadyClosedException.class); + } + + @Test + void txParentROChildRWIncompatible() { + assertThatThrownBy( + () -> { + try (Txn txRoot = env.txnRead()) { + env.txn(txRoot, new TxnFlags[0]); // error + } + }) + .isInstanceOf(IncompatibleParent.class); + } + + @Test + void txParentRWChildROIncompatible() { + assertThatThrownBy( + () -> { + try (Txn txRoot = env.txnWrite()) { + TxnFlags[] flags = new TxnFlags[] {MDB_RDONLY_TXN}; + env.txn(txRoot, flags); // error + } + }) + .isInstanceOf(IncompatibleParent.class); + } +} diff --git a/src/test/java/org/lmdbjava/TxnFlagSetTest.java b/src/test/java/org/lmdbjava/TxnFlagSetTest.java new file mode 100644 index 00000000..37068202 --- /dev/null +++ b/src/test/java/org/lmdbjava/TxnFlagSetTest.java @@ -0,0 +1,88 @@ +/* + * 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 static org.lmdbjava.TxnFlags.MDB_RDONLY_TXN; + +import java.util.Arrays; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +class TxnFlagSetTest extends AbstractFlagSetTest { + + @Test + void test() { + // This is here purely to stop CodeQL moaning that this class is unused. + // All the actual tests are in the superclass + assertThat(getAllFlags()).isNotNull(); + } + + @Override + List getAllFlags() { + return Arrays.stream(TxnFlags.values()).collect(Collectors.toList()); + } + + @Override + TxnFlagSet getEmptyFlagSet() { + return TxnFlagSet.empty(); + } + + @Override + AbstractFlagSet.Builder getBuilder() { + return TxnFlagSet.builder(); + } + + @Override + TxnFlagSet getFlagSet(Collection flags) { + return TxnFlagSet.of(flags); + } + + @Override + TxnFlagSet getFlagSet(TxnFlags[] flags) { + return TxnFlagSet.of(flags); + } + + @Override + TxnFlagSet getFlagSet(TxnFlags flag) { + return TxnFlagSet.of(flag); + } + + @Override + Class getFlagType() { + return TxnFlags.class; + } + + @Override + Function, TxnFlagSet> getConstructor() { + return TxnFlagSetImpl::new; + } + + /** + * {@link FlagSet#isSet(MaskedFlag)} on the flag enum is tested in {@link AbstractFlagSetTest} but + * the coverage check doesn't seem to notice it. + */ + @Test + void testIsSet() { + assertThat(MDB_RDONLY_TXN.isSet(MDB_RDONLY_TXN)).isTrue(); + //noinspection ConstantValue + assertThat(MDB_RDONLY_TXN.isSet(null)).isFalse(); + } +} diff --git a/src/test/java/org/lmdbjava/TxnTest.java b/src/test/java/org/lmdbjava/TxnTest.java index 46ffeb70..7210b613 100644 --- a/src/test/java/org/lmdbjava/TxnTest.java +++ b/src/test/java/org/lmdbjava/TxnTest.java @@ -16,7 +16,6 @@ package org.lmdbjava; -import static com.jakewharton.byteunits.BinaryByteUnit.KIBIBYTES; import static java.nio.ByteBuffer.allocateDirect; import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; @@ -27,7 +26,6 @@ import static org.lmdbjava.EnvFlags.MDB_RDONLY_ENV; import static org.lmdbjava.KeyRange.closed; import static org.lmdbjava.TestUtils.DB_1; -import static org.lmdbjava.TestUtils.POSIX_MODE; import static org.lmdbjava.TestUtils.bb; import static org.lmdbjava.Txn.State.DONE; import static org.lmdbjava.Txn.State.READY; @@ -35,7 +33,6 @@ import static org.lmdbjava.Txn.State.RESET; import static org.lmdbjava.TxnFlags.MDB_RDONLY_TXN; -import java.io.IOException; import java.nio.ByteBuffer; import java.nio.file.Path; import java.util.ArrayList; @@ -60,28 +57,37 @@ public final class TxnTest { private Path file; private Env env; + private TempDir tempDir; + @BeforeEach void beforeEach() { - file = FileUtil.createTempFile(); + tempDir = new TempDir(); + file = tempDir.createTempFile(); env = create() - .setMapSize(KIBIBYTES.toBytes(256)) + .setMapSize(256, ByteUnit.KIBIBYTES) .setMaxReaders(1) .setMaxDbs(2) - .open(file.toFile(), POSIX_MODE, MDB_NOSUBDIR); + .setEnvFlags(MDB_NOSUBDIR) + .open(file); } @AfterEach void afterEach() { env.close(); - FileUtil.delete(file); + tempDir.cleanup(); } @Test - void largeKeysRejected() throws IOException { + void largeKeysRejected() { assertThatThrownBy( () -> { - final Dbi dbi = env.openDbi(DB_1, MDB_CREATE); + final Dbi dbi = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); final ByteBuffer key = allocateDirect(env.getMaxKeySize() + 1); key.limit(key.capacity()); dbi.put(key, bb(2)); @@ -91,7 +97,8 @@ void largeKeysRejected() throws IOException { @Test void rangeSearch() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); final ByteBuffer key = allocateDirect(env.getMaxKeySize()); key.put("cherry".getBytes(UTF_8)).flip(); @@ -125,9 +132,9 @@ void rangeSearch() { @Test void readOnlyTxnAllowedInReadOnlyEnv() { - env.openDbi(DB_1, MDB_CREATE); + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Env roEnv = - create().setMaxReaders(1).open(file.toFile(), MDB_NOSUBDIR, MDB_RDONLY_ENV)) { + create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { assertThat(roEnv.txnRead()).isNotNull(); } } @@ -136,10 +143,14 @@ void readOnlyTxnAllowedInReadOnlyEnv() { void readWriteTxnDeniedInReadOnlyEnv() { assertThatThrownBy( () -> { - env.openDbi(DB_1, MDB_CREATE); + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); env.close(); try (Env roEnv = - create().setMaxReaders(1).open(file.toFile(), MDB_NOSUBDIR, MDB_RDONLY_ENV)) { + create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { roEnv.txnWrite(); // error } }) @@ -182,8 +193,8 @@ void testCheckWritesAllowed() { @Test void testGetId() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); final AtomicLong txId1 = new AtomicLong(); final AtomicLong txId2 = new AtomicLong(); @@ -310,6 +321,26 @@ public void txParent() { } } + @Test + public void txParent2() { + try (Txn txRoot = env.txnWrite()) { + assertThatThrownBy( + () -> { + env.txn(txRoot, (TxnFlagSet) null); + }) + .isInstanceOf(NullPointerException.class); + } + } + + @Test + public void txParent3() { + try (Txn txRoot = env.txnWrite(); + Txn txChild = env.txn(txRoot, TxnFlagSet.EMPTY)) { + assertThat(txRoot.getParent()).isNull(); + assertThat(txChild.getParent()).isEqualTo(txRoot); + } + } + @Test void txParentDeniedIfEnvClosed() { assertThatThrownBy( @@ -418,7 +449,12 @@ void txResetDeniedForReadWriteTransaction() { void zeroByteKeysRejected() { assertThatThrownBy( () -> { - final Dbi dbi = env.openDbi(DB_1, MDB_CREATE); + final Dbi dbi = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); final ByteBuffer key = allocateDirect(4); key.putInt(1); assertThat(key.remaining()).isEqualTo(0); // because key.flip() skipped diff --git a/src/test/java/org/lmdbjava/VerifierTest.java b/src/test/java/org/lmdbjava/VerifierTest.java index 64214568..ee396084 100644 --- a/src/test/java/org/lmdbjava/VerifierTest.java +++ b/src/test/java/org/lmdbjava/VerifierTest.java @@ -16,13 +16,14 @@ 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 java.nio.ByteBuffer; +import java.nio.file.Path; import java.util.concurrent.TimeUnit; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; /** Test {@link Verifier}. */ @@ -30,18 +31,32 @@ public final class VerifierTest { @Test void verification() { - FileUtil.useTempFile( - file -> { - try (Env env = - create() - .setMaxReaders(1) - .setMaxDbs(Verifier.DBI_COUNT) - .setMapSize(MEBIBYTES.toBytes(10)) - .open(file.toFile(), MDB_NOSUBDIR)) { - final Verifier v = new Verifier(env); - final int seconds = Integer.getInteger("verificationSeconds", 2); - assertThat(v.runFor(seconds, TimeUnit.SECONDS)).isGreaterThan(1L); - } - }); + try (final TempDir tempDir = new TempDir()) { + final Path file = tempDir.createTempFile(); + try (Env env = + create() + .setMaxReaders(1) + .setMaxDbs(Verifier.DBI_COUNT) + .setMapSize(10, ByteUnit.MEBIBYTES) + .setEnvFlags(MDB_NOSUBDIR) + .open(file)) { + + // Create a DB to ensure that the verifier can c + env.createDbi() + .setDbName("db1") + .withDefaultComparator() + .setDbiFlags(DbiFlags.MDB_CREATE) + .open(); + + Assertions.assertThat(env.getDbiNames(Env.DEFAULT_NAME_CHARSET)).containsExactly("db1"); + + final Verifier v = new Verifier(env); + + Assertions.assertThat(env.getDbiNames(Env.DEFAULT_NAME_CHARSET)).doesNotContain("db1"); + + final int seconds = Integer.getInteger("verificationSeconds", 2); + assertThat(v.runFor(seconds, TimeUnit.SECONDS)).isGreaterThan(1L); + } + } } } diff --git a/src/test/resources/CursorIterableRangeTest/testIntegerKey.csv b/src/test/resources/CursorIterableRangeTest/testIntegerKey.csv new file mode 100644 index 00000000..df61bf73 --- /dev/null +++ b/src/test/resources/CursorIterableRangeTest/testIntegerKey.csv @@ -0,0 +1,53 @@ +FORWARD_ALL,,,[0 1][1000 2][1000000 3][-1000000 4][-1000 5] +FORWARD_AT_LEAST,999,,[1000 2][1000000 3][-1000000 4][-1000 5] +FORWARD_AT_LEAST,1000,,[1000 2][1000000 3][-1000000 4][-1000 5] +FORWARD_AT_LEAST,1001,,[1000000 3][-1000000 4][-1000 5] +FORWARD_AT_MOST,,999,[0 1] +FORWARD_AT_MOST,,1000,[0 1][1000 2] +FORWARD_AT_MOST,,1001,[0 1][1000 2] +FORWARD_CLOSED,999,1001,[1000 2] +FORWARD_CLOSED,1000,1000,[1000 2] +FORWARD_CLOSED_OPEN,999,1001,[1000 2] +FORWARD_CLOSED_OPEN,1000,1000, +FORWARD_CLOSED_OPEN,1000,1001,[1000 2] +FORWARD_GREATER_THAN,999,,[1000 2][1000000 3][-1000000 4][-1000 5] +FORWARD_GREATER_THAN,1000,,[1000000 3][-1000000 4][-1000 5] +FORWARD_GREATER_THAN,1001,,[1000000 3][-1000000 4][-1000 5] +FORWARD_LESS_THAN,,999,[0 1] +FORWARD_LESS_THAN,,1000,[0 1] +FORWARD_LESS_THAN,,1001,[0 1][1000 2] +FORWARD_OPEN,999,1001,[1000 2] +FORWARD_OPEN,999,1000, +FORWARD_OPEN,1000,1000, +FORWARD_OPEN,1000,1001, +FORWARD_OPEN_CLOSED,999,1001,[1000 2] +FORWARD_OPEN_CLOSED,999,1000,[1000 2] +FORWARD_OPEN_CLOSED,1000,1000, +FORWARD_OPEN_CLOSED,1000,1001, +BACKWARD_ALL,,,[-1000 5][-1000000 4][1000000 3][1000 2][0 1] +BACKWARD_AT_LEAST,999,,[0 1] +BACKWARD_AT_LEAST,1000,,[1000 2][0 1] +BACKWARD_AT_LEAST,1001,,[1000 2][0 1] +BACKWARD_AT_MOST,,999,[-1000 5][-1000000 4][1000000 3][1000 2] +BACKWARD_AT_MOST,,1000,[-1000 5][-1000000 4][1000000 3][1000 2] +BACKWARD_AT_MOST,,1001,[-1000 5][-1000000 4][1000000 3] +BACKWARD_CLOSED,1001,999,[1000 2] +BACKWARD_CLOSED,1000,1000,[1000 2] +BACKWARD_CLOSED_OPEN,1001,999,[1000 2] +BACKWARD_CLOSED_OPEN,1000,999,[1000 2] +BACKWARD_CLOSED_OPEN,1000,1000, +BACKWARD_CLOSED_OPEN,1001,1000, +BACKWARD_GREATER_THAN,999,,[0 1] +BACKWARD_GREATER_THAN,1000,,[0 1] +BACKWARD_GREATER_THAN,1001,,[1000 2][0 1] +BACKWARD_LESS_THAN,,999,[-1000 5][-1000000 4][1000000 3][1000 2] +BACKWARD_LESS_THAN,,1000,[-1000 5][-1000000 4][1000000 3] +BACKWARD_LESS_THAN,,1001,[-1000 5][-1000000 4][1000000 3] +BACKWARD_OPEN,1001,999,[1000 2] +BACKWARD_OPEN,1000,999, +BACKWARD_OPEN,1000,1000, +BACKWARD_OPEN,1001,1000, +BACKWARD_OPEN_CLOSED,1001,999,[1000 2] +BACKWARD_OPEN_CLOSED,1000,999, +BACKWARD_OPEN_CLOSED,1000,1000, +BACKWARD_OPEN_CLOSED,1001,1000,[1000 2] diff --git a/src/test/resources/CursorIterableRangeTest/testLongKey.csv b/src/test/resources/CursorIterableRangeTest/testLongKey.csv new file mode 100644 index 00000000..df61bf73 --- /dev/null +++ b/src/test/resources/CursorIterableRangeTest/testLongKey.csv @@ -0,0 +1,53 @@ +FORWARD_ALL,,,[0 1][1000 2][1000000 3][-1000000 4][-1000 5] +FORWARD_AT_LEAST,999,,[1000 2][1000000 3][-1000000 4][-1000 5] +FORWARD_AT_LEAST,1000,,[1000 2][1000000 3][-1000000 4][-1000 5] +FORWARD_AT_LEAST,1001,,[1000000 3][-1000000 4][-1000 5] +FORWARD_AT_MOST,,999,[0 1] +FORWARD_AT_MOST,,1000,[0 1][1000 2] +FORWARD_AT_MOST,,1001,[0 1][1000 2] +FORWARD_CLOSED,999,1001,[1000 2] +FORWARD_CLOSED,1000,1000,[1000 2] +FORWARD_CLOSED_OPEN,999,1001,[1000 2] +FORWARD_CLOSED_OPEN,1000,1000, +FORWARD_CLOSED_OPEN,1000,1001,[1000 2] +FORWARD_GREATER_THAN,999,,[1000 2][1000000 3][-1000000 4][-1000 5] +FORWARD_GREATER_THAN,1000,,[1000000 3][-1000000 4][-1000 5] +FORWARD_GREATER_THAN,1001,,[1000000 3][-1000000 4][-1000 5] +FORWARD_LESS_THAN,,999,[0 1] +FORWARD_LESS_THAN,,1000,[0 1] +FORWARD_LESS_THAN,,1001,[0 1][1000 2] +FORWARD_OPEN,999,1001,[1000 2] +FORWARD_OPEN,999,1000, +FORWARD_OPEN,1000,1000, +FORWARD_OPEN,1000,1001, +FORWARD_OPEN_CLOSED,999,1001,[1000 2] +FORWARD_OPEN_CLOSED,999,1000,[1000 2] +FORWARD_OPEN_CLOSED,1000,1000, +FORWARD_OPEN_CLOSED,1000,1001, +BACKWARD_ALL,,,[-1000 5][-1000000 4][1000000 3][1000 2][0 1] +BACKWARD_AT_LEAST,999,,[0 1] +BACKWARD_AT_LEAST,1000,,[1000 2][0 1] +BACKWARD_AT_LEAST,1001,,[1000 2][0 1] +BACKWARD_AT_MOST,,999,[-1000 5][-1000000 4][1000000 3][1000 2] +BACKWARD_AT_MOST,,1000,[-1000 5][-1000000 4][1000000 3][1000 2] +BACKWARD_AT_MOST,,1001,[-1000 5][-1000000 4][1000000 3] +BACKWARD_CLOSED,1001,999,[1000 2] +BACKWARD_CLOSED,1000,1000,[1000 2] +BACKWARD_CLOSED_OPEN,1001,999,[1000 2] +BACKWARD_CLOSED_OPEN,1000,999,[1000 2] +BACKWARD_CLOSED_OPEN,1000,1000, +BACKWARD_CLOSED_OPEN,1001,1000, +BACKWARD_GREATER_THAN,999,,[0 1] +BACKWARD_GREATER_THAN,1000,,[0 1] +BACKWARD_GREATER_THAN,1001,,[1000 2][0 1] +BACKWARD_LESS_THAN,,999,[-1000 5][-1000000 4][1000000 3][1000 2] +BACKWARD_LESS_THAN,,1000,[-1000 5][-1000000 4][1000000 3] +BACKWARD_LESS_THAN,,1001,[-1000 5][-1000000 4][1000000 3] +BACKWARD_OPEN,1001,999,[1000 2] +BACKWARD_OPEN,1000,999, +BACKWARD_OPEN,1000,1000, +BACKWARD_OPEN,1001,1000, +BACKWARD_OPEN_CLOSED,1001,999,[1000 2] +BACKWARD_OPEN_CLOSED,1000,999, +BACKWARD_OPEN_CLOSED,1000,1000, +BACKWARD_OPEN_CLOSED,1001,1000,[1000 2] diff --git a/src/test/resources/CursorIterableRangeTest/testSignedComparator.csv b/src/test/resources/CursorIterableRangeTest/testSignedComparator.csv new file mode 100644 index 00000000..fc341231 --- /dev/null +++ b/src/test/resources/CursorIterableRangeTest/testSignedComparator.csv @@ -0,0 +1,49 @@ +FORWARD_ALL,,,[-2 -1][0 1][2 3][4 5][6 7][8 9] +FORWARD_AT_LEAST,5,,[6 7][8 9] +FORWARD_AT_LEAST,6,,[6 7][8 9] +FORWARD_AT_MOST,,5,[-2 -1][0 1][2 3][4 5] +FORWARD_AT_MOST,,6,[-2 -1][0 1][2 3][4 5][6 7] +FORWARD_CLOSED,3,7,[4 5][6 7] +FORWARD_CLOSED,2,6,[2 3][4 5][6 7] +FORWARD_CLOSED,1,7,[2 3][4 5][6 7] +FORWARD_CLOSED_OPEN,3,8,[4 5][6 7] +FORWARD_CLOSED_OPEN,2,6,[2 3][4 5] +FORWARD_GREATER_THAN,4,,[6 7][8 9] +FORWARD_GREATER_THAN,3,,[4 5][6 7][8 9] +FORWARD_LESS_THAN,,5,[-2 -1][0 1][2 3][4 5] +FORWARD_LESS_THAN,,8,[-2 -1][0 1][2 3][4 5][6 7] +FORWARD_OPEN,3,7,[4 5][6 7] +FORWARD_OPEN,2,8,[4 5][6 7] +FORWARD_OPEN_CLOSED,3,8,[4 5][6 7][8 9] +FORWARD_OPEN_CLOSED,2,6,[4 5][6 7] +BACKWARD_ALL,,,[8 9][6 7][4 5][2 3][0 1][-2 -1] +BACKWARD_AT_LEAST,5,,[4 5][2 3][0 1][-2 -1] +BACKWARD_AT_LEAST,6,,[6 7][4 5][2 3][0 1][-2 -1] +BACKWARD_AT_LEAST,9,,[8 9][6 7][4 5][2 3][0 1][-2 -1] +BACKWARD_AT_LEAST,-1,,[-2 -1] +BACKWARD_AT_MOST,,5,[8 9][6 7] +BACKWARD_AT_MOST,,6,[8 9][6 7] +BACKWARD_AT_MOST,,-1,[8 9][6 7][4 5][2 3][0 1] +BACKWARD_CLOSED,7,3,[6 7][4 5] +BACKWARD_CLOSED,6,2,[6 7][4 5][2 3] +BACKWARD_CLOSED,9,3,[8 9][6 7][4 5] +BACKWARD_CLOSED,9,-1,[8 9][6 7][4 5][2 3][0 1] +BACKWARD_CLOSED_OPEN,8,3,[8 9][6 7][4 5] +BACKWARD_CLOSED_OPEN,7,2,[6 7][4 5] +BACKWARD_CLOSED_OPEN,9,3,[8 9][6 7][4 5] +BACKWARD_CLOSED_OPEN,9,-1,[8 9][6 7][4 5][2 3][0 1] +BACKWARD_GREATER_THAN,6,,[4 5][2 3][0 1][-2 -1] +BACKWARD_GREATER_THAN,7,,[6 7][4 5][2 3][0 1][-2 -1] +BACKWARD_GREATER_THAN,9,,[8 9][6 7][4 5][2 3][0 1][-2 -1] +BACKWARD_GREATER_THAN,-1,,[-2 -1] +BACKWARD_LESS_THAN,,5,[8 9][6 7] +BACKWARD_LESS_THAN,,2,[8 9][6 7][4 5] +BACKWARD_LESS_THAN,,-1,[8 9][6 7][4 5][2 3][0 1] +BACKWARD_OPEN,7,2,[6 7][4 5] +BACKWARD_OPEN,8,1,[6 7][4 5][2 3] +BACKWARD_OPEN,9,4,[8 9][6 7] +BACKWARD_OPEN,9,-1,[8 9][6 7][4 5][2 3][0 1] +BACKWARD_OPEN_CLOSED,7,2,[6 7][4 5][2 3] +BACKWARD_OPEN_CLOSED,8,4,[6 7][4 5] +BACKWARD_OPEN_CLOSED,9,4,[8 9][6 7][4 5] +BACKWARD_OPEN_CLOSED,9,-1,[8 9][6 7][4 5][2 3][0 1] diff --git a/src/test/resources/CursorIterableRangeTest/testSignedComparatorDupsort.csv b/src/test/resources/CursorIterableRangeTest/testSignedComparatorDupsort.csv new file mode 100644 index 00000000..1a18f426 --- /dev/null +++ b/src/test/resources/CursorIterableRangeTest/testSignedComparatorDupsort.csv @@ -0,0 +1,49 @@ +FORWARD_ALL,,,[-2 0][-2 -1][0 1][0 2][2 3][2 4][4 5][4 6][6 7][6 8][8 9][8 10] +FORWARD_AT_LEAST,5,,[6 7][6 8][8 9][8 10] +FORWARD_AT_LEAST,6,,[6 7][6 8][8 9][8 10] +FORWARD_AT_MOST,,5,[-2 0][-2 -1][0 1][0 2][2 3][2 4][4 5][4 6] +FORWARD_AT_MOST,,6,[-2 0][-2 -1][0 1][0 2][2 3][2 4][4 5][4 6][6 7][6 8] +FORWARD_CLOSED,3,7,[4 5][4 6][6 7][6 8] +FORWARD_CLOSED,2,6,[2 3][2 4][4 5][4 6][6 7][6 8] +FORWARD_CLOSED,1,7,[2 3][2 4][4 5][4 6][6 7][6 8] +FORWARD_CLOSED_OPEN,3,8,[4 5][4 6][6 7][6 8] +FORWARD_CLOSED_OPEN,2,6,[2 3][2 4][4 5][4 6] +FORWARD_GREATER_THAN,4,,[6 7][6 8][8 9][8 10] +FORWARD_GREATER_THAN,3,,[4 5][4 6][6 7][6 8][8 9][8 10] +FORWARD_LESS_THAN,,5,[-2 0][-2 -1][0 1][0 2][2 3][2 4][4 5][4 6] +FORWARD_LESS_THAN,,8,[-2 0][-2 -1][0 1][0 2][2 3][2 4][4 5][4 6][6 7][6 8] +FORWARD_OPEN,3,7,[4 5][4 6][6 7][6 8] +FORWARD_OPEN,2,8,[4 5][4 6][6 7][6 8] +FORWARD_OPEN_CLOSED,3,8,[4 5][4 6][6 7][6 8][8 9][8 10] +FORWARD_OPEN_CLOSED,2,6,[4 5][4 6][6 7][6 8] +BACKWARD_ALL,,,[8 10][8 9][6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1][-2 -1][-2 0] +BACKWARD_AT_LEAST,5,,[4 6][4 5][2 4][2 3][0 2][0 1][-2 -1][-2 0] +BACKWARD_AT_LEAST,6,,[6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1][-2 -1][-2 0] +BACKWARD_AT_LEAST,9,,[8 10][8 9][6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1][-2 -1][-2 0] +BACKWARD_AT_LEAST,-1,,[-2 -1][-2 0] +BACKWARD_AT_MOST,,5,[8 10][8 9][6 8][6 7] +BACKWARD_AT_MOST,,6,[8 10][8 9][6 8][6 7] +BACKWARD_AT_MOST,,-1,[8 10][8 9][6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1] +BACKWARD_CLOSED,7,3,[6 8][6 7][4 6][4 5] +BACKWARD_CLOSED,6,2,[6 8][6 7][4 6][4 5][2 4][2 3] +BACKWARD_CLOSED,9,3,[8 10][8 9][6 8][6 7][4 6][4 5] +BACKWARD_CLOSED,9,-1,[8 10][8 9][6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1] +BACKWARD_CLOSED_OPEN,8,3,[8 10][8 9][6 8][6 7][4 6][4 5] +BACKWARD_CLOSED_OPEN,7,2,[6 8][6 7][4 6][4 5] +BACKWARD_CLOSED_OPEN,9,3,[8 10][8 9][6 8][6 7][4 6][4 5] +BACKWARD_CLOSED_OPEN,9,-1,[8 10][8 9][6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1] +BACKWARD_GREATER_THAN,6,,[4 6][4 5][2 4][2 3][0 2][0 1][-2 -1][-2 0] +BACKWARD_GREATER_THAN,7,,[6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1][-2 -1][-2 0] +BACKWARD_GREATER_THAN,9,,[8 10][8 9][6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1][-2 -1][-2 0] +BACKWARD_GREATER_THAN,-1,,[-2 -1][-2 0] +BACKWARD_LESS_THAN,,5,[8 10][8 9][6 8][6 7] +BACKWARD_LESS_THAN,,2,[8 10][8 9][6 8][6 7][4 6][4 5] +BACKWARD_LESS_THAN,,-1,[8 10][8 9][6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1] +BACKWARD_OPEN,7,2,[6 8][6 7][4 6][4 5] +BACKWARD_OPEN,8,1,[6 8][6 7][4 6][4 5][2 4][2 3] +BACKWARD_OPEN,9,4,[8 10][8 9][6 8][6 7] +BACKWARD_OPEN,9,-1,[8 10][8 9][6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1] +BACKWARD_OPEN_CLOSED,7,2,[6 8][6 7][4 6][4 5][2 4][2 3] +BACKWARD_OPEN_CLOSED,8,4,[6 8][6 7][4 6][4 5] +BACKWARD_OPEN_CLOSED,9,4,[8 10][8 9][6 8][6 7][4 6][4 5] +BACKWARD_OPEN_CLOSED,9,-1,[8 10][8 9][6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1] diff --git a/src/test/resources/CursorIterableRangeTest/testUnsignedComparator.csv b/src/test/resources/CursorIterableRangeTest/testUnsignedComparator.csv new file mode 100644 index 00000000..905efcff --- /dev/null +++ b/src/test/resources/CursorIterableRangeTest/testUnsignedComparator.csv @@ -0,0 +1,49 @@ +FORWARD_ALL,,,[0 1][2 3][4 5][6 7][8 9][-2 -1] +FORWARD_AT_LEAST,5,,[6 7][8 9][-2 -1] +FORWARD_AT_LEAST,6,,[6 7][8 9][-2 -1] +FORWARD_AT_MOST,,5,[0 1][2 3][4 5] +FORWARD_AT_MOST,,6,[0 1][2 3][4 5][6 7] +FORWARD_CLOSED,3,7,[4 5][6 7] +FORWARD_CLOSED,2,6,[2 3][4 5][6 7] +FORWARD_CLOSED,1,7,[2 3][4 5][6 7] +FORWARD_CLOSED_OPEN,3,8,[4 5][6 7] +FORWARD_CLOSED_OPEN,2,6,[2 3][4 5] +FORWARD_GREATER_THAN,4,,[6 7][8 9][-2 -1] +FORWARD_GREATER_THAN,3,,[4 5][6 7][8 9][-2 -1] +FORWARD_LESS_THAN,,5,[0 1][2 3][4 5] +FORWARD_LESS_THAN,,8,[0 1][2 3][4 5][6 7] +FORWARD_OPEN,3,7,[4 5][6 7] +FORWARD_OPEN,2,8,[4 5][6 7] +FORWARD_OPEN_CLOSED,3,8,[4 5][6 7][8 9] +FORWARD_OPEN_CLOSED,2,6,[4 5][6 7] +BACKWARD_ALL,,,[-2 -1][8 9][6 7][4 5][2 3][0 1] +BACKWARD_AT_LEAST,5,,[4 5][2 3][0 1] +BACKWARD_AT_LEAST,6,,[6 7][4 5][2 3][0 1] +BACKWARD_AT_LEAST,9,,[8 9][6 7][4 5][2 3][0 1] +BACKWARD_AT_LEAST,-1,,[-2 -1][8 9][6 7][4 5][2 3][0 1] +BACKWARD_AT_MOST,,5,[-2 -1][8 9][6 7] +BACKWARD_AT_MOST,,6,[-2 -1][8 9][6 7] +BACKWARD_AT_MOST,,-1, +BACKWARD_CLOSED,7,3,[6 7][4 5] +BACKWARD_CLOSED,6,2,[6 7][4 5][2 3] +BACKWARD_CLOSED,9,3,[8 9][6 7][4 5] +BACKWARD_CLOSED,9,-1, +BACKWARD_CLOSED_OPEN,8,3,[8 9][6 7][4 5] +BACKWARD_CLOSED_OPEN,7,2,[6 7][4 5] +BACKWARD_CLOSED_OPEN,9,3,[8 9][6 7][4 5] +BACKWARD_CLOSED_OPEN,9,-1, +BACKWARD_GREATER_THAN,6,,[4 5][2 3][0 1] +BACKWARD_GREATER_THAN,7,,[6 7][4 5][2 3][0 1] +BACKWARD_GREATER_THAN,9,,[8 9][6 7][4 5][2 3][0 1] +BACKWARD_GREATER_THAN,-1,,[-2 -1][8 9][6 7][4 5][2 3][0 1] +BACKWARD_LESS_THAN,,5,[-2 -1][8 9][6 7] +BACKWARD_LESS_THAN,,2,[-2 -1][8 9][6 7][4 5] +BACKWARD_LESS_THAN,,-1, +BACKWARD_OPEN,7,2,[6 7][4 5] +BACKWARD_OPEN,8,1,[6 7][4 5][2 3] +BACKWARD_OPEN,9,4,[8 9][6 7] +BACKWARD_OPEN,9,-1, +BACKWARD_OPEN_CLOSED,7,2,[6 7][4 5][2 3] +BACKWARD_OPEN_CLOSED,8,4,[6 7][4 5] +BACKWARD_OPEN_CLOSED,9,4,[8 9][6 7][4 5] +BACKWARD_OPEN_CLOSED,9,-1, diff --git a/src/test/resources/CursorIterableRangeTest/testUnsignedComparatorDupsort.csv b/src/test/resources/CursorIterableRangeTest/testUnsignedComparatorDupsort.csv new file mode 100644 index 00000000..e9054cbd --- /dev/null +++ b/src/test/resources/CursorIterableRangeTest/testUnsignedComparatorDupsort.csv @@ -0,0 +1,60 @@ +FORWARD_ALL,,,[0 1][0 2][2 3][2 4][4 5][4 6][6 7][6 8][8 9][8 10][-2 0][-2 -1] +FORWARD_AT_LEAST,5,,[6 7][6 8][8 9][8 10][-2 0][-2 -1] +FORWARD_AT_LEAST,6,,[6 7][6 8][8 9][8 10][-2 0][-2 -1] +FORWARD_AT_MOST,,5,[0 1][0 2][2 3][2 4][4 5][4 6] +FORWARD_AT_MOST,,6,[0 1][0 2][2 3][2 4][4 5][4 6][6 7][6 8] +FORWARD_CLOSED,3,7,[4 5][4 6][6 7][6 8] +FORWARD_CLOSED,2,6,[2 3][2 4][4 5][4 6][6 7][6 8] +FORWARD_CLOSED,1,7,[2 3][2 4][4 5][4 6][6 7][6 8] +FORWARD_CLOSED_OPEN,3,8,[4 5][4 6][6 7][6 8] +FORWARD_CLOSED_OPEN,2,6,[2 3][2 4][4 5][4 6] +FORWARD_GREATER_THAN,4,,[6 7][6 8][8 9][8 10][-2 0][-2 -1] +FORWARD_GREATER_THAN,3,,[4 5][4 6][6 7][6 8][8 9][8 10][-2 0][-2 -1] +FORWARD_LESS_THAN,,5,[0 1][0 2][2 3][2 4][4 5][4 6] +FORWARD_LESS_THAN,,8,[0 1][0 2][2 3][2 4][4 5][4 6][6 7][6 8] +FORWARD_OPEN,3,7,[4 5][4 6][6 7][6 8] +FORWARD_OPEN,2,8,[4 5][4 6][6 7][6 8] +FORWARD_OPEN_CLOSED,3,8,[4 5][4 6][6 7][6 8][8 9][8 10] +FORWARD_OPEN_CLOSED,2,6,[4 5][4 6][6 7][6 8] +BACKWARD_ALL,,,[-2 -1][-2 0][8 10][8 9][6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1] +BACKWARD_AT_LEAST,5,,[4 6][4 5][2 4][2 3][0 2][0 1] +BACKWARD_AT_LEAST,6,,[6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1] +BACKWARD_AT_LEAST,9,,[8 10][8 9][6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1] +BACKWARD_AT_LEAST,-1,,[-2 -1][-2 0][8 10][8 9][6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1] +BACKWARD_AT_MOST,,5,[-2 -1][-2 0][8 10][8 9][6 8][6 7] +BACKWARD_AT_MOST,,6,[-2 -1][-2 0][8 10][8 9][6 8][6 7] +BACKWARD_AT_MOST,,-1, +BACKWARD_CLOSED,7,3,[6 8][6 7][4 6][4 5] +BACKWARD_CLOSED,6,2,[6 8][6 7][4 6][4 5][2 4][2 3] +BACKWARD_CLOSED,9,3,[8 10][8 9][6 8][6 7][4 6][4 5] +BACKWARD_CLOSED,9,-1, +BACKWARD_CLOSED_OPEN,8,3,[8 10][8 9][6 8][6 7][4 6][4 5] +BACKWARD_CLOSED_OPEN,7,2,[6 8][6 7][4 6][4 5] +BACKWARD_CLOSED_OPEN,9,3,[8 10][8 9][6 8][6 7][4 6][4 5] +BACKWARD_CLOSED_OPEN,9,-1, +BACKWARD_GREATER_THAN,6,,[4 6][4 5][2 4][2 3][0 2][0 1] +BACKWARD_GREATER_THAN,7,,[6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1] +BACKWARD_GREATER_THAN,9,,[8 10][8 9][6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1] +BACKWARD_GREATER_THAN,-1,,[-2 -1][-2 0][8 10][8 9][6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1] +BACKWARD_LESS_THAN,,5,[-2 -1][-2 0][8 10][8 9][6 8][6 7] +BACKWARD_LESS_THAN,,2,[-2 -1][-2 0][8 10][8 9][6 8][6 7][4 6][4 5] +BACKWARD_LESS_THAN,,-1, +BACKWARD_OPEN,7,2,[6 8][6 7][4 6][4 5] +BACKWARD_OPEN,8,1,[6 8][6 7][4 6][4 5][2 4][2 3] +BACKWARD_OPEN,9,4,[8 10][8 9][6 8][6 7] +BACKWARD_OPEN,9,-1, +BACKWARD_OPEN_CLOSED,7,2,[6 8][6 7][4 6][4 5][2 4][2 3] +BACKWARD_OPEN_CLOSED,8,4,[6 8][6 7][4 6][4 5] +BACKWARD_OPEN_CLOSED,9,4,[8 10][8 9][6 8][6 7][4 6][4 5] +BACKWARD_OPEN_CLOSED,9,-1, +# +# TEST gh-267 +BACKWARD_AT_LEAST,6,,[6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1] +BACKWARD_AT_LEAST,-2,,[-2 -1][-2 0][8 10][8 9][6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1] +BACKWARD_AT_LEAST,9,,[8 10][8 9][6 8][6 7][4 6][4 5][2 4][2 3][0 2][0 1] +BACKWARD_CLOSED,6,3,[6 8][6 7][4 6][4 5] +BACKWARD_CLOSED,-2,2,[-2 -1][-2 0][8 10][8 9][6 8][6 7][4 6][4 5][2 4][2 3] +BACKWARD_CLOSED,9,3,[8 10][8 9][6 8][6 7][4 6][4 5] +BACKWARD_CLOSED_OPEN,6,2,[6 8][6 7][4 6][4 5] +BACKWARD_CLOSED_OPEN,-2,3,[-2 -1][-2 0][8 10][8 9][6 8][6 7][4 6][4 5] +BACKWARD_CLOSED_OPEN,9,-1, \ No newline at end of file