diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index dd93488f..b503370a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,15 +27,15 @@ jobs: uses: actions/checkout@v3 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} queries: +security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 32668e32..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 ] @@ -13,34 +14,22 @@ jobs: steps: - name: Check out Git repository - uses: actions/checkout@v3 + uses: actions/checkout@v5 - name: Set up Java and Maven - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: distribution: zulu - java-version: 21 + java-version: 25 cache: maven - - name: Install Zig - uses: goto-bus-stop/setup-zig@v2 - - - name: Cross compile using Zig - run: ./cross-compile.sh - - name: Build with Maven run: mvn -B verify -DgcRecordWrites=1000 - - name: Store built native libraries for later jobs - uses: actions/upload-artifact@v3 - with: - name: native-libraries - path: | - src/main/resources/org/lmdbjava/*.so - src/main/resources/org/lmdbjava/*.dll - - name: Upload code coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_ORG_TOKEN }} compatibility-checks: name: Java ${{ matrix.java }} on ${{ matrix.os }} Compatibility @@ -50,74 +39,69 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - java: [8, 11, 17, 21] + java: [8, 11, 17, 21, 25] steps: - name: Check out Git repository - uses: actions/checkout@v3 + uses: actions/checkout@v5 - name: Set up Java and Maven - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: distribution: zulu java-version: ${{ matrix.java }} cache: maven - - name: Fetch built native libraries - uses: actions/download-artifact@v3 - with: - name: native-libraries - path: src/main/resources/org/lmdbjava - - name: Execute verifier run: mvn -B test -Dtest=VerifierTest -DverificationSeconds=10 - name: Upload Surefire reports on test failure - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: surefire-test-log path: target/surefire-reports deploy: - name: Deploy to OSSRH + name: Deploy to Central Portal needs: [build, compatibility-checks] if: github.event_name == 'push' runs-on: ubuntu-latest steps: - name: Check out Git repository - uses: actions/checkout@v3 + uses: actions/checkout@v5 - name: Set up Java and Maven - uses: actions/setup-java@v3 + uses: actions/setup-java@v5 with: distribution: zulu # Java version 8 required due to https://github.com/lmdbjava/lmdbjava/issues/116 java-version: 8 cache: maven - server-id: ossrh - server-username: MAVEN_USERNAME - server-password: MAVEN_CENTRAL_TOKEN + server-id: central + server-username: MAVEN_CENTRAL_USERNAME + server-password: MAVEN_CENTRAL_PASSWORD gpg-private-key: ${{ secrets.gpg_private_key }} gpg-passphrase: MAVEN_GPG_PASSPHRASE - - name: Install Zig - uses: goto-bus-stop/setup-zig@v2 - - - name: Cross compile using Zig - run: ./cross-compile.sh + - name: Get project version + id: version + run: echo "version=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)" >> $GITHUB_OUTPUT - name: Publish Maven package - run: mvn -B -Possrh-deploy deploy -DskipTests + 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 }} - MAVEN_USERNAME: ${{ secrets.nexus_username }} - MAVEN_CENTRAL_TOKEN: ${{ secrets.nexus_password }} + MAVEN_CENTRAL_USERNAME: ${{ secrets.central_username }} + MAVEN_CENTRAL_PASSWORD: ${{ secrets.central_password }} - name: Debug settings.xml - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: settings.xml - path: $HOME/.m2/settings.xml + path: ~/.m2/settings.xml diff --git a/.gitignore b/.gitignore index 0c771329..5f3ff273 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,4 @@ dependency-reduced-pom.xml gpg-sign.json mvn-sync.json secrets.tar -lmdb +pom.xml.versionsBackup diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53a6b14e..09448c03 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,12 +6,20 @@ We welcome patches and pull requests to improve LmdbJava. This will run: * Tests -* Initial Test Coverage -* Checkstyle -* PMD -* FindBugs -* XML Formatting +* Source Code Formatting * License Header Management `mvn clean verify` is also run by CI, but it's quicker and easier to run before submitting. + +### Version Management + +Update all dependency and plugin versions: +```bash +mvn versions:update-properties +``` + +### Releasing + +GitHub Actions will perform an official release whenever a developer executes +`mvn release:clean release:prepare`. diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 00000000..77fad436 --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,36 @@ +Copyright © 2016-2025 The LmdbJava Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +--- + +The LmdbJava project distribution JAR file includes a distribution of [LMDB](https://www.symas.com/mdb), which is licensed under the [OpenLDAP Public License](https://www.openldap.org/software/release/license.html). + +--- + +# Dependencies + +LmdbJava uses the following libraries. +All license files can also be found in the `licenses` directory in the root of this repository. + +| Dependency | License | +|-------------|----------| +| `com.github.jnr:jnr-constants` | [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) | +| `com.github.jnr:jnr-ffi` | [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) | +| `com.google.guava:guava` | [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) | +| `com.jakewharton.byteunits:byteunits` | [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) | +| `io.netty:netty-buffer` | [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) | +| `junit:junit` | [Eclipse Public License 1.0](https://www.eclipse.org/org/documents/epl-v10.html) | +| `org.agrona:agrona` | [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) | +| `org.hamcrest:hamcrest` | [BSD 3-Clause License](https://opensource.org/license/bsd-3-clause) | +| `org.mockito:mockito-inline` | [MIT License](https://opensource.org/license/mit) | diff --git a/README.md b/README.md index 048e57bc..adf3fbbf 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ **LmdbJava** adds Java-specific features to LMDB: -* [Extremely fast](https://github.com/lmdbjava/benchmarks/blob/master/results/20160710/README.md) across a broad range of benchmarks, data sizes and access patterns +* [Extremely fast](https://github.com/lmdbjava/benchmarks) across a broad range of benchmarks, data sizes and access patterns * Modern, idiomatic Java API (including iterators, key ranges, enums, exceptions etc) * Nothing to install (the JAR embeds the latest LMDB libraries for Linux, OS X and Windows) * Buffer agnostic (Java `ByteBuffer`, Agrona `DirectBuffer`, Netty `ByteBuf`, your own buffer) @@ -31,8 +31,8 @@ * Low latency design (allocation-free; buffer pools; optional checks can be easily disabled in production etc) * Mature code (commenced in 2016) and used for heavy production workloads (eg > 500 TB of HFT data) * Actively maintained and with a "Zero Bug Policy" before every release (see [issues](https://github.com/lmdbjava/lmdbjava/issues)) -* Available from [Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.lmdbjava%22%20AND%20a%3A%22lmdbjava%22) and [OSS Sonatype Snapshots](https://oss.sonatype.org/content/repositories/snapshots/org/lmdbjava/lmdbjava) -* [Continuous integration](https://github.com/lmdbjava/lmdbjava/actions) testing on Linux, Windows and macOS with Java 8, 11, 17 and 21 +* Available from [Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.lmdbjava%22%20AND%20a%3A%22lmdbjava%22) and [Central Portal Snapshots](https://central.sonatype.com/repository/maven-snapshots/org/lmdbjava/lmdbjava) +* [Continuous integration](https://github.com/lmdbjava/lmdbjava/actions) testing on Linux, Windows and macOS with Java 8, 11, 17, 21 and 25 ### Performance @@ -57,21 +57,10 @@ any questions. ### Building -This project uses [Zig](https://ziglang.org/) to cross-compile the LMDB native -library for all supported architectures. To locally build LmdbJava you must -firstly install a recent version of Zig and then execute the project's -[cross-compile.sh](https://github.com/lmdbjava/lmdbjava/tree/master/cross-compile.sh) -script. This only needs to be repeated when the `cross-compile.sh` script is -updated (eg following a new official release of the upstream LMDB library). - -If you do not wish to install Zig and/or use an operating system which cannot -easily execute the `cross-compile.sh` script, you can download the compiled -LMDB native library for your platform from a location of your choice and set the -`lmdbjava.native.lib` system property to the resulting file system system -location. Possible sources of a compiled LMDB native library include operating -system package managers, running `cross-compile.sh` on a supported system, or -copying it from the `org/lmdbjava` directory of any recent, officially released -LmdbJava JAR. +LmdbJava uses a standard Maven build. Its native libraries are provided by the +[`org.lmdbjava:native`](https://github.com/lmdbjava/native) dependency. + +To use a different LMDB library, set `lmdbjava.native.lib` system property to the file path. ### Contributing diff --git a/cross-compile.sh b/cross-compile.sh deleted file mode 100755 index 1ebf92f6..00000000 --- a/cross-compile.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -set -o errexit - -rm -rf lmdb -git clone --depth 1 --branch LMDB_0.9.29 https://github.com/LMDB/lmdb.git -pushd lmdb/libraries/liblmdb -trap popd SIGINT - -# zig targets | jq -r '.libc[]' -for target in aarch64-linux-gnu \ - aarch64-macos-none \ - x86_64-linux-gnu \ - x86_64-macos-none \ - x86_64-windows-gnu -do - echo "##### Building $target ####" - make -e clean liblmdb.so CC="zig cc -target $target" AR="zig ar" - if [[ "$target" == *-windows-* ]]; then - extension="dll" - else - extension="so" - fi - cp -v liblmdb.so ../../../src/main/resources/org/lmdbjava/$target.$extension -done - -ls -l ../../../src/main/resources/org/lmdbjava diff --git a/licenses/Apache-2.0 b/licenses/Apache-2.0 new file mode 100644 index 00000000..6ca24c75 --- /dev/null +++ b/licenses/Apache-2.0 @@ -0,0 +1,51 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of this License; and +You must cause any modified files to carry prominent notices stating that You changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/licenses/OpenLDAP-2.8.txt b/licenses/OpenLDAP-2.8.txt new file mode 100644 index 00000000..05ad7571 --- /dev/null +++ b/licenses/OpenLDAP-2.8.txt @@ -0,0 +1,47 @@ +The OpenLDAP Public License + Version 2.8, 17 August 2003 + +Redistribution and use of this software and associated documentation +("Software"), with or without modification, are permitted provided +that the following conditions are met: + +1. Redistributions in source form must retain copyright statements + and notices, + +2. Redistributions in binary form must reproduce applicable copyright + statements and notices, this list of conditions, and the following + disclaimer in the documentation and/or other materials provided + with the distribution, and + +3. Redistributions must contain a verbatim copy of this document. + +The OpenLDAP Foundation may revise this license from time to time. +Each revision is distinguished by a version number. You may use +this Software under terms of this license revision or under the +terms of any subsequent revision of the license. + +THIS SOFTWARE IS PROVIDED BY THE OPENLDAP FOUNDATION AND ITS +CONTRIBUTORS ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL THE OPENLDAP FOUNDATION, ITS CONTRIBUTORS, OR THE AUTHOR(S) +OR OWNER(S) OF THE SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +The names of the authors and copyright holders must not be used in +advertising or otherwise to promote the sale, use or other dealing +in this Software without specific, written prior permission. Title +to copyright in this Software shall at all times remain with copyright +holders. + +OpenLDAP is a registered trademark of the OpenLDAP Foundation. + +Copyright 1999-2003 The OpenLDAP Foundation, Redwood City, +California, USA. All Rights Reserved. Permission to copy and +distribute verbatim copies of this document is granted. diff --git a/pom.xml b/pom.xml index 75ce0fe6..5c1501c1 100644 --- a/pom.xml +++ b/pom.xml @@ -1,152 +1,403 @@ + 4.0.0 - - au.com.acegi - acegi-standard-project - 0.7.0 - org.lmdbjava lmdbjava - 0.9.1-SNAPSHOT + 1.0.0-SNAPSHOT jar LmdbJava Low latency Java API for the ultra-fast, embedded Symas Lightning Database (LMDB) - lmdbjava - lmdbjava - apache_v2 + + 1.22.0 + 3.27.7 + 3.2.1 + 0.9.0 + 2.29 + 1.28.0 + 33.5.0-jre + 0.8.14 + 0.10.4 + 2.2.18 + 5.14.1 + 4.6 + 0.9.33-5 + 3.5.0 + 3.14.1 + 3.9.0 + 3.1.4 + 3.6.2 + 3.2.7 + 3.1.4 + 3.4.2 + 3.12.0 + 3.2.0 + 3.3.1 + 2.2.1 + 3.21.0 + 3.3.1 + 3.5.4 + yyyy-MM-dd'T'HH:mm:ss'Z' + 1.8 + 1.8 + 1.8 + 3.5.4 + 4.11.0 + + 4.1.118.Final + UTF-8 + 4.0.0 + false + 2.19.1 com.github.jnr jnr-constants - 0.10.4 + ${jnr-constants.version} com.github.jnr jnr-ffi - 2.2.15 - - - com.google.code.findbugs - annotations + ${jnr-ffi.version} com.google.guava guava - 32.1.3-jre - test - - - com.google.code.findbugs - jsr305 - - - - - com.jakewharton.byteunits - byteunits - 0.9.1 + ${guava.version} test io.netty netty-buffer - 4.1.101.Final + ${netty-buffer.version} true - - junit - junit - org.agrona agrona - 1.20.0 + ${agrona.version} true - org.hamcrest - hamcrest + org.assertj + assertj-core + ${assertj.version} + test + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + + + org.lmdbjava + native + ${lmdbjava-native.version} org.mockito mockito-inline - 4.11.0 + ${mockito.version} test - au.com.acegi - xml-format-maven-plugin + com.mycila + license-maven-plugin + ${license-maven-plugin.version} + + + +
src/misc/license-template.txt
+ + LICENSE.txt + **/*.md + **/*.csv + lmdb/** + licenses/** + +
+
+ true +
+ + + com.mycila + license-maven-plugin-git + ${license-maven-plugin.version} + + + + + apply-license + + format + + package + + 2 + + + +
+ + org.apache.maven.plugins + maven-clean-plugin + ${maven-clean-plugin.version} + + false + - com.github.spotbugs - spotbugs-maven-plugin + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} org.apache.maven.plugins maven-dependency-plugin - - - com.github.jnr:jffi - org.mockito:mockito-core - org.mockito:mockito-inline - - + ${maven-dependency-plugin.version} + + + + analyze-only + properties + + + false + + com.github.jnr:jffi + org.mockito:mockito-core + org.mockito:mockito-inline + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + ${maven-deploy-plugin.version} org.apache.maven.plugins maven-enforcer-plugin + ${maven-enforcer-plugin.version} + + + config-enforcer + + enforce + + + + + + [${maven.enforcer.mvn},) + + + [${maven.enforcer.java},) + + + + + true + + + + + + + + + org.apache.maven.plugins + maven-install-plugin + ${maven-install-plugin.version} org.apache.maven.plugins maven-jar-plugin + ${maven-jar-plugin.version} + + true + - org.lmdbjava + ${buildNumber} org.apache.maven.plugins - maven-pmd-plugin + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + attach-javadocs + + jar + + + org.apache.maven.plugins - maven-surefire-plugin + maven-release-plugin + ${maven-release-plugin.version} - 1 - false + clean + clean + false + true - org.codehaus.mojo - buildnumber-maven-plugin + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + + org.apache.maven.plugins + maven-site-plugin + ${maven-site-plugin.version} + + true + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} org.codehaus.mojo - license-maven-plugin + buildnumber-maven-plugin + ${buildnumber-maven-plugin.version} + + false + false + UNKNOWN + + jgit + + true + 7 + + + + org.apache.maven.scm + maven-scm-provider-jgit + ${maven-scm-provider-jgit.version} + + + + + + create + + initialize + + org.codehaus.mojo versions-maven-plugin + ${versions-maven-plugin.version} + + false + false + false + agrona.version,netty-buffer.version + + + + regex + .*-alpha.* + + + regex + .*-beta.* + + + regex + .*-M.* + + + regex + .*-RC.* + + + + org.jacoco jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + config-jacoco-prepare-agent + + prepare-agent + + + + config-jacoco-report + + report + + +
+ https://github.com/lmdbjava/lmdbjava 2016 The LmdbJava Open Source Project - https://github.com/${github.org} + https://github.com/lmdbjava @@ -156,35 +407,24 @@ - krisskross - Kristoffer Sjogren - stoffe -at- gmail.com - http://stoffe.deephacks.org/ - - +1 - - - benalexau - Ben Alex - ben.alex@acegi.com.au - https://github.com/benalexau/ - Acegi Technology Pty Limited - +10 + lmdbjava + The LmdbJava Open Source Project + https://github.com/lmdbjava - scm:git:git@github.com:${github.org}/${github.repo}.git - scm:git:git@github.com:${github.org}/${github.repo}.git - git@github.com:${github.org}/${github.repo}.git + scm:git:git@github.com:lmdbjava/lmdbjava.git + scm:git:git@github.com:lmdbjava/lmdbjava.git + git@github.com:lmdbjava/lmdbjava.git HEAD GitHub Issues - https://github.com/${github.org}/${github.repo}/issues + https://github.com/lmdbjava/lmdbjava/issues GitHub Actions - https://github.com/${github.org}/${github.repo}/actions + https://github.com/lmdbjava/lmdbjava/actions @@ -198,7 +438,7 @@ org.apache.maven.plugins maven-surefire-plugin - --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED + @{argLine} --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED 1 false @@ -216,14 +456,78 @@ com.github.ekryd.sortpom sortpom-maven-plugin + ${sortpom-maven-plugin.version} + + ${project.basedir}/pom.xml + ${project.build.sourceEncoding} + custom_1 + true + groupId,artifactId + groupId,artifactId + true + false + false + \n + false + + + + + sort + + prepare-package + + + + + + com.spotify.fmt + fmt-maven-plugin + ${fmt-maven-plugin.version} + + + com.google.googlejavaformat + google-java-format + ${google-java-format.version} + + + + + + + central-deploy + + org.apache.maven.plugins - maven-checkstyle-plugin + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + sign-artifacts + + sign + + verify + + + --pinentry-mode + loopback + + + + - org.basepom.maven - duplicate-finder-maven-plugin + org.sonatype.central + central-publishing-maven-plugin + ${central-publishing-maven-plugin.version} + true + + central + 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 aff732bc..a3c339bf 100644 --- a/src/main/java/org/lmdbjava/BufferProxy.java +++ b/src/main/java/org/lmdbjava/BufferProxy.java @@ -1,79 +1,55 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static java.lang.Long.BYTES; import java.util.Comparator; - import jnr.ffi.Pointer; /** * The strategy for mapping memory address to a given buffer type. * - *

- * The proxy is passed to the {@link Env#create(org.lmdbjava.BufferProxy)} - * method and is subsequently used by every {@link Txn}, {@link Dbi} and - * {@link Cursor} associated with the {@link Env}. + *

The proxy is passed to the {@link Env#create(org.lmdbjava.BufferProxy)} method and is + * subsequently used by every {@link Txn}, {@link Dbi} and {@link Cursor} associated with the {@link + * Env}. * * @param buffer type */ -@SuppressWarnings("checkstyle:AbstractClassName") public abstract class BufferProxy { - /** - * Size of a MDB_val pointer in bytes. - */ + /** Size of a MDB_val pointer in bytes. */ protected static final int MDB_VAL_STRUCT_SIZE = BYTES * 2; - /** - * Offset from a pointer of the MDB_val.mv_data field. - */ + /** Offset from a pointer of the MDB_val.mv_data field. */ protected static final int STRUCT_FIELD_OFFSET_DATA = BYTES; - /** - * Offset from a pointer of the MDB_val.mv_size field. - */ + /** Offset from a pointer of the MDB_val.mv_size field. */ protected static final int STRUCT_FIELD_OFFSET_SIZE = 0; + /** Explicitly-defined default constructor to avoid warnings. */ + protected BufferProxy() {} + /** - * Allocate a new buffer suitable for passing to - * {@link #out(java.lang.Object, jnr.ffi.Pointer, long)}. + * Allocate a new buffer suitable for passing to {@link #out(java.lang.Object, jnr.ffi.Pointer)}. * * @return a buffer for passing to the out method */ protected abstract T allocate(); - /** - * Get a suitable default {@link Comparator} given the provided flags. - * - *

- * The provided comparator must strictly match the lexicographical order of - * keys in the native LMDB database. - * - * @param flags for the database - * @return a comparator that can be used (never null) - */ - protected abstract Comparator getComparator(DbiFlags... flags); - /** * Deallocate a buffer that was previously provided by {@link #allocate()}. * @@ -90,37 +66,57 @@ public abstract class BufferProxy { protected abstract byte[] getBytes(T buffer); /** - * Called when the MDB_val should be set to reflect the passed - * buffer. This buffer will have been created by end users, not - * {@link #allocate()}. + * Get a suitable default {@link Comparator} given the provided flags. + * + *

The provided comparator must strictly match the lexicographical order of keys in the native + * LMDB database. + * + * @param dbiFlagSet The {@link DbiFlags} set for the database. + * @return a comparator that can be used (never null) + */ + public abstract Comparator getComparator(final DbiFlagSet dbiFlagSet); + + /** + * Get a suitable default {@link Comparator} + * + *

The provided comparator must strictly match the lexicographical order of keys in the native + * LMDB database. + * + * @return a comparator that can be used (never null) + */ + public Comparator getComparator() { + return getComparator(DbiFlagSet.empty()); + } + + /** + * Called when the MDB_val should be set to reflect the passed buffer. This buffer + * will have been created by end users, not {@link #allocate()}. * - * @param buffer the buffer to write to MDB_val - * @param ptr the pointer to the MDB_val - * @param ptrAddr the address of the MDB_val pointer + * @param buffer the buffer to write to MDB_val + * @param ptr the pointer to the MDB_val + * @return a transient pointer that must be kept alive, or null if none */ - protected abstract void in(T buffer, Pointer ptr, long ptrAddr); + protected abstract Pointer in(T buffer, Pointer ptr); /** - * Called when the MDB_val should be set to reflect the passed - * buffer. + * Called when the MDB_val should be set to reflect the passed buffer. * - * @param buffer the buffer to write to MDB_val - * @param size the buffer size to write to MDB_val - * @param ptr the pointer to the MDB_val - * @param ptrAddr the address of the MDB_val pointer + * @param buffer the buffer to write to MDB_val + * @param size the buffer size to write to MDB_val + * @param ptr the pointer to the MDB_val + * @return a transient pointer that must be kept alive, or null if none */ - protected abstract void in(T buffer, int size, Pointer ptr, long ptrAddr); + protected abstract Pointer in(T buffer, int size, Pointer ptr); /** - * Called when the MDB_val may have changed and the passed buffer - * should be modified to reflect the new MDB_val. + * Called when the MDB_val may have changed and the passed buffer should be modified + * to reflect the new MDB_val. * - * @param buffer the buffer to write to MDB_val - * @param ptr the pointer to the MDB_val - * @param ptrAddr the address of the MDB_val pointer + * @param buffer the buffer to write to MDB_val + * @param ptr the pointer to the MDB_val * @return the buffer for MDB_val */ - protected abstract T out(T buffer, Pointer ptr, long ptrAddr); + protected abstract T out(T buffer, Pointer ptr); /** * Create a new {@link KeyVal} to hold pointers for this buffer proxy. @@ -131,4 +127,12 @@ final KeyVal keyVal() { return new KeyVal<>(this); } + /** + * Create a new {@link Key} to hold pointers for this buffer proxy. + * + * @return a non-null key holder + */ + final Key key() { + return new Key<>(this); + } } diff --git a/src/main/java/org/lmdbjava/ByteArrayProxy.java b/src/main/java/org/lmdbjava/ByteArrayProxy.java index 3fe8184a..82b7721c 100644 --- a/src/main/java/org/lmdbjava/ByteArrayProxy.java +++ b/src/main/java/org/lmdbjava/ByteArrayProxy.java @@ -1,50 +1,42 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; +import static java.lang.Math.min; import static java.util.Objects.requireNonNull; import static org.lmdbjava.Library.RUNTIME; import java.util.Arrays; import java.util.Comparator; - import jnr.ffi.Pointer; import jnr.ffi.provider.MemoryManager; /** * Byte array proxy. * - * {@link Env#create(org.lmdbjava.BufferProxy)}. + *

{@link Env#create(org.lmdbjava.BufferProxy)}. */ public final class ByteArrayProxy extends BufferProxy { - /** - * The byte array proxy. Guaranteed to never be null. - */ + /** The byte array proxy. Guaranteed to never be null. */ public static final BufferProxy PROXY_BA = new ByteArrayProxy(); private static final MemoryManager MEM_MGR = RUNTIME.getMemoryManager(); - private ByteArrayProxy() { - } + private ByteArrayProxy() {} /** * Lexicographically compare two byte arrays. @@ -53,14 +45,13 @@ private ByteArrayProxy() { * @param o2 right operand (required) * @return as specified by {@link Comparable} interface */ - @SuppressWarnings("PMD.CompareObjectsWithEquals") - public static int compareArrays(final byte[] o1, final byte[] o2) { + public static int compareLexicographically(final byte[] o1, final byte[] o2) { requireNonNull(o1); requireNonNull(o2); if (o1 == o2) { return 0; } - final int minLength = Math.min(o1.length, o2.length); + final int minLength = min(o1.length, o2.length); for (int i = 0; i < minLength; i++) { final int lw = Byte.toUnsignedInt(o1[i]); @@ -79,10 +70,6 @@ protected byte[] allocate() { return new byte[0]; } - protected int compare(final byte[] o1, final byte[] o2) { - return compareArrays(o1, o2); - } - @Override protected void deallocate(final byte[] buff) { // byte arrays cannot be allocated @@ -94,28 +81,27 @@ protected byte[] getBytes(final byte[] buffer) { } @Override - protected Comparator getComparator(final DbiFlags... flags) { - return this::compare; + public Comparator getComparator(final DbiFlagSet dbiFlagSet) { + return ByteArrayProxy::compareLexicographically; } @Override - protected void in(final byte[] buffer, final Pointer ptr, - final long ptrAddr) { + protected Pointer in(final byte[] buffer, final Pointer ptr) { final Pointer pointer = MEM_MGR.allocateDirect(buffer.length); pointer.put(0, buffer, 0, buffer.length); ptr.putLong(STRUCT_FIELD_OFFSET_SIZE, buffer.length); ptr.putAddress(STRUCT_FIELD_OFFSET_DATA, pointer.address()); + return pointer; } @Override - protected void in(final byte[] buffer, final int size, final Pointer ptr, - final long ptrAddr) { + protected Pointer in(final byte[] buffer, final int size, final Pointer ptr) { // cannot reserve for byte arrays + return null; } @Override - protected byte[] out(final byte[] buffer, final Pointer ptr, - final long ptrAddr) { + protected byte[] out(final byte[] buffer, final Pointer ptr) { final long addr = ptr.getAddress(STRUCT_FIELD_OFFSET_DATA); final int size = (int) ptr.getLong(STRUCT_FIELD_OFFSET_SIZE); final Pointer pointer = MEM_MGR.newPointer(addr, size); diff --git a/src/main/java/org/lmdbjava/ByteBufProxy.java b/src/main/java/org/lmdbjava/ByteBufProxy.java index 26351676..bcbb6ebf 100644 --- a/src/main/java/org/lmdbjava/ByteBufProxy.java +++ b/src/main/java/org/lmdbjava/ByteBufProxy.java @@ -1,49 +1,43 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static io.netty.buffer.PooledByteBufAllocator.DEFAULT; import static java.lang.Class.forName; +import static java.util.Objects.requireNonNull; import static org.lmdbjava.UnsafeAccess.UNSAFE; -import java.lang.reflect.Field; -import java.util.Comparator; - import io.netty.buffer.ByteBuf; import io.netty.buffer.PooledByteBufAllocator; +import java.lang.reflect.Field; +import java.nio.ByteOrder; +import java.util.Comparator; import jnr.ffi.Pointer; /** * A buffer proxy backed by Netty's {@link ByteBuf}. * - *

- * This class requires {@link UnsafeAccess} and netty-buffer must be in the - * classpath. + *

This class requires {@link UnsafeAccess} and netty-buffer must be in the classpath. */ public final class ByteBufProxy extends BufferProxy { /** - * A proxy for using Netty {@link ByteBuf}. Guaranteed to never be null, - * although a class initialization exception will occur if an attempt is made - * to access this field when Netty is unavailable. + * A proxy for using Netty {@link ByteBuf}. Guaranteed to never be null, although a class + * initialization exception will occur if an attempt is made to access this field when Netty is + * unavailable. */ public static final BufferProxy PROXY_NETTY = new ByteBufProxy(); @@ -53,14 +47,20 @@ public final class ByteBufProxy extends BufferProxy { private static final String NAME = "io.netty.buffer.PooledUnsafeDirectByteBuf"; private final long lengthOffset; private final long addressOffset; - + private final PooledByteBufAllocator nettyAllocator; private ByteBufProxy() { this(DEFAULT); } + /** + * Constructs a buffer proxy for use with Netty. + * + * @param allocator the Netty allocator to obtain the {@link ByteBuf} from + */ public ByteBufProxy(final PooledByteBufAllocator allocator) { + super(); this.nettyAllocator = allocator; try { @@ -75,6 +75,71 @@ public ByteBufProxy(final PooledByteBufAllocator allocator) { } } + /** + * Lexicographically compare two buffers. + * + * @param o1 left operand (required) + * @param o2 right operand (required) + * @return as specified by {@link Comparable} interface + */ + public static int compareLexicographically(final ByteBuf o1, final ByteBuf o2) { + requireNonNull(o1); + requireNonNull(o2); + return o1.compareTo(o2); + } + + /** + * Buffer comparator specifically for 4/8 byte keys that are unsigned ints/longs, i.e. when using + * MDB_INTEGER_KEY/MDB_INTEGERDUP. Compares the buffers numerically. + * + * @param o1 left operand (required) + * @param o2 right operand (required) + * @return as specified by {@link Comparable} interface + */ + public static int compareAsIntegerKeys(final ByteBuf o1, final ByteBuf o2) { + requireNonNull(o1); + requireNonNull(o2); + // Both buffers should be same length according to LMDB API. + // From the LMDB docs for MDB_INTEGER_KEY + // numeric keys in native byte order: either unsigned int or size_t. The keys must all be of the + // same size. + final int len1 = o1.readableBytes(); + final int len2 = o2.readableBytes(); + if (len1 != len2) { + throw new RuntimeException( + "Length mismatch, len1: " + + len1 + + ", len2: " + + len2 + + ". Lengths must be identical and either 4 or 8 bytes."); + } + if (len1 == 8) { + final long lw; + final long rw; + if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) { + lw = o1.readLongLE(); + rw = o2.readLongLE(); + } else { + lw = o1.readLong(); + rw = o2.readLong(); + } + return Long.compareUnsigned(lw, rw); + } else if (len1 == 4) { + final int lw; + final int rw; + if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) { + lw = o1.readIntLE(); + rw = o2.readIntLE(); + } else { + lw = o1.readInt(); + rw = o2.readInt(); + } + return Integer.compareUnsigned(lw, rw); + } else { + return compareLexicographically(o1, o2); + } + } + static Field findField(final String c, final String name) { Class clazz; try { @@ -107,13 +172,13 @@ protected ByteBuf allocate() { throw new IllegalStateException("Netty buffer must be " + NAME); } - protected int compare(final ByteBuf o1, final ByteBuf o2) { - return o1.compareTo(o2); - } - @Override - protected Comparator getComparator(final DbiFlags... flags) { - return this::compare; + public Comparator getComparator(final DbiFlagSet dbiFlagSet) { + if (dbiFlagSet.areAnySet(DbiFlagSet.INTEGER_KEY_FLAGS)) { + return ByteBufProxy::compareAsIntegerKeys; + } else { + return ByteBufProxy::compareLexicographically; + } } @Override @@ -129,25 +194,26 @@ protected byte[] getBytes(final ByteBuf buffer) { } @Override - protected void in(final ByteBuf buffer, final Pointer ptr, final long ptrAddr) { - UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE, - buffer.writerIndex() - buffer.readerIndex()); - UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_DATA, - buffer.memoryAddress() + buffer.readerIndex()); + protected Pointer in(final ByteBuf buffer, final Pointer ptr) { + final long ptrAddr = ptr.address(); + UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE, buffer.writerIndex() - buffer.readerIndex()); + UNSAFE.putLong( + ptrAddr + STRUCT_FIELD_OFFSET_DATA, buffer.memoryAddress() + buffer.readerIndex()); + return null; } @Override - protected void in(final ByteBuf buffer, final int size, final Pointer ptr, - final long ptrAddr) { - UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE, - size); - UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_DATA, - buffer.memoryAddress() + buffer.readerIndex()); + protected Pointer in(final ByteBuf buffer, final int size, final Pointer ptr) { + final long ptrAddr = ptr.address(); + UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE, size); + UNSAFE.putLong( + ptrAddr + STRUCT_FIELD_OFFSET_DATA, buffer.memoryAddress() + buffer.readerIndex()); + return null; } @Override - protected ByteBuf out(final ByteBuf buffer, final Pointer ptr, - final long ptrAddr) { + protected ByteBuf out(final ByteBuf buffer, final Pointer ptr) { + final long ptrAddr = ptr.address(); final long addr = UNSAFE.getLong(ptrAddr + STRUCT_FIELD_OFFSET_DATA); final long size = UNSAFE.getLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE); UNSAFE.putLong(buffer, addressOffset, addr); diff --git a/src/main/java/org/lmdbjava/ByteBufferProxy.java b/src/main/java/org/lmdbjava/ByteBufferProxy.java index 8eb95da8..b5dfca0b 100644 --- a/src/main/java/org/lmdbjava/ByteBufferProxy.java +++ b/src/main/java/org/lmdbjava/ByteBufferProxy.java @@ -1,23 +1,18 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static java.lang.Long.reverseBytes; @@ -26,58 +21,51 @@ import static java.nio.ByteOrder.BIG_ENDIAN; import static java.nio.ByteOrder.LITTLE_ENDIAN; import static java.util.Objects.requireNonNull; -import static org.lmdbjava.DbiFlags.MDB_INTEGERKEY; import static org.lmdbjava.Env.SHOULD_CHECK; -import static org.lmdbjava.MaskedFlag.isSet; -import static org.lmdbjava.MaskedFlag.mask; import static org.lmdbjava.UnsafeAccess.UNSAFE; import java.lang.reflect.Field; import java.nio.Buffer; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.ArrayDeque; import java.util.Comparator; - import jnr.ffi.Pointer; /** * {@link ByteBuffer}-based proxy. * - *

- * There are two concrete {@link ByteBuffer} proxy implementations available: + *

There are two concrete {@link ByteBuffer} proxy implementations available: + * *

    - *
  • A "fast" implementation: {@link UnsafeProxy}
  • - *
  • A "safe" implementation: {@link ReflectiveProxy}
  • + *
  • A "fast" implementation: {@link UnsafeProxy} + *
  • A "safe" implementation: {@link ReflectiveProxy} *
* - *

- * Users nominate which implementation they prefer by referencing the - * {@link #PROXY_OPTIMAL} or {@link #PROXY_SAFE} field when invoking - * {@link Env#create(org.lmdbjava.BufferProxy)}. + *

Users nominate which implementation they prefer by referencing the {@link #PROXY_OPTIMAL} or + * {@link #PROXY_SAFE} field when invoking {@link Env#create(org.lmdbjava.BufferProxy)}. */ public final class ByteBufferProxy { /** - * The fastest {@link ByteBuffer} proxy that is available on this platform. - * This will always be the same instance as {@link #PROXY_SAFE} if the - * {@link UnsafeAccess#DISABLE_UNSAFE_PROP} has been set to true - * and/or {@link UnsafeAccess} is unavailable. Guaranteed to never be null. + * The fastest {@link ByteBuffer} proxy that is available on this platform. This will always be + * the same instance as {@link #PROXY_SAFE} if the {@link UnsafeAccess#DISABLE_UNSAFE_PROP} has + * been set to true and/or {@link UnsafeAccess} is unavailable. Guaranteed to never + * be null. */ public static final BufferProxy PROXY_OPTIMAL; - /** - * The safe, reflective {@link ByteBuffer} proxy for this system. Guaranteed - * to never be null. - */ + /** The safe, reflective {@link ByteBuffer} proxy for this system. Guaranteed to never be null. */ public static final BufferProxy PROXY_SAFE; + private static final ByteOrder NATIVE_ORDER = ByteOrder.nativeOrder(); + static { PROXY_SAFE = new ReflectiveProxy(); PROXY_OPTIMAL = getProxyOptimal(); } - private ByteBufferProxy() { - } + private ByteBufferProxy() {} private static BufferProxy getProxyOptimal() { try { @@ -87,24 +75,20 @@ private static BufferProxy getProxyOptimal() { } } - /** - * The buffer must be a direct buffer (not heap allocated). - */ + /** The buffer must be a direct buffer (not heap allocated). */ public static final class BufferMustBeDirectException extends LmdbException { private static final long serialVersionUID = 1L; - /** - * Creates a new instance. - */ + /** Creates a new instance. */ public BufferMustBeDirectException() { super("The buffer must be a direct buffer (not heap allocated"); } } /** - * Provides {@link ByteBuffer} pooling and address resolution for concrete - * {@link BufferProxy} implementations. + * Provides {@link ByteBuffer} pooling and address resolution for concrete {@link BufferProxy} + * implementations. */ abstract static class AbstractByteBufferProxy extends BufferProxy { @@ -112,12 +96,11 @@ abstract static class AbstractByteBufferProxy extends BufferProxy { protected static final String FIELD_NAME_CAPACITY = "capacity"; /** - * A thread-safe pool for a given length. If the buffer found is valid (ie - * not of a negative length) then that buffer is used. If no valid buffer is - * found, a new buffer is created. + * A thread-safe pool for a given length. If the buffer found is valid (ie not of a negative + * length) then that buffer is used. If no valid buffer is found, a new buffer is created. */ - private static final ThreadLocal> BUFFERS - = withInitial(() -> new ArrayDeque<>(16)); + private static final ThreadLocal> BUFFERS = + withInitial(() -> new ArrayDeque<>(16)); /** * Lexicographically compare two buffers. @@ -126,13 +109,10 @@ abstract static class AbstractByteBufferProxy extends BufferProxy { * @param o2 right operand (required) * @return as specified by {@link Comparable} interface */ - @SuppressWarnings("PMD.CyclomaticComplexity") - public static int compareBuff(final ByteBuffer o1, final ByteBuffer o2) { + public static int compareLexicographically(final ByteBuffer o1, final ByteBuffer o2) { requireNonNull(o1); requireNonNull(o2); - if (o1.equals(o2)) { - return 0; - } + final int minLength = Math.min(o1.limit(), o2.limit()); final int minWords = minLength / Long.BYTES; @@ -159,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 { @@ -193,22 +222,12 @@ protected final ByteBuffer allocate() { } @Override - protected Comparator getComparator(final DbiFlags... flags) { - final int flagInt = mask(flags); - if (isSet(flagInt, MDB_INTEGERKEY)) { - return this::compareCustom; + public Comparator getComparator(final DbiFlagSet dbiFlagSet) { + if (dbiFlagSet.areAnySet(DbiFlagSet.INTEGER_KEY_FLAGS)) { + return AbstractByteBufferProxy::compareAsIntegerKeys; + } else { + return AbstractByteBufferProxy::compareLexicographically; } - return this::compareDefault; - } - - protected final int compareDefault(final ByteBuffer o1, - final ByteBuffer o2) { - return o1.compareTo(o2); - } - - protected final int compareCustom(final ByteBuffer o1, - final ByteBuffer o2) { - return compareBuff(o1, o2); } @Override @@ -224,12 +243,11 @@ protected byte[] getBytes(final ByteBuffer buffer) { buffer.get(dest, 0, buffer.limit()); return dest; } - } /** - * A proxy that uses Java reflection to modify byte buffer fields, and - * official JNR-FFF methods to manipulate native pointers. + * A proxy that uses Java reflection to modify byte buffer fields, and official JNR-FFF methods to + * manipulate native pointers. */ private static final class ReflectiveProxy extends AbstractByteBufferProxy { @@ -242,22 +260,22 @@ private static final class ReflectiveProxy extends AbstractByteBufferProxy { } @Override - protected void in(final ByteBuffer buffer, final Pointer ptr, - final long ptrAddr) { + protected Pointer in(final ByteBuffer buffer, final Pointer ptr) { ptr.putAddress(STRUCT_FIELD_OFFSET_DATA, address(buffer)); ptr.putLong(STRUCT_FIELD_OFFSET_SIZE, buffer.remaining()); + return null; } @Override - protected void in(final ByteBuffer buffer, final int size, final Pointer ptr, - final long ptrAddr) { + protected Pointer in(final ByteBuffer buffer, final int size, final Pointer ptr) { ptr.putLong(STRUCT_FIELD_OFFSET_SIZE, size); ptr.putAddress(STRUCT_FIELD_OFFSET_DATA, address(buffer)); + return null; } @Override - protected ByteBuffer out(final ByteBuffer buffer, final Pointer ptr, - final long ptrAddr) { + protected ByteBuffer out(final ByteBuffer buffer, final Pointer ptr) { + final long ptrAddr = ptr.address(); final long addr = ptr.getAddress(STRUCT_FIELD_OFFSET_DATA); final long size = ptr.getLong(STRUCT_FIELD_OFFSET_SIZE); try { @@ -269,12 +287,11 @@ protected ByteBuffer out(final ByteBuffer buffer, final Pointer ptr, buffer.clear(); return buffer; } - } /** - * A proxy that uses Java's "unsafe" class to directly manipulate byte buffer - * fields and JNR-FFF allocated memory pointers. + * A proxy that uses Java's "unsafe" class to directly manipulate byte buffer fields and JNR-FFF + * allocated memory pointers. */ private static final class UnsafeProxy extends AbstractByteBufferProxy { @@ -293,22 +310,24 @@ private static final class UnsafeProxy extends AbstractByteBufferProxy { } @Override - protected void in(final ByteBuffer buffer, final Pointer ptr, - final long ptrAddr) { + protected Pointer in(final ByteBuffer buffer, final Pointer ptr) { + final long ptrAddr = ptr.address(); UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE, buffer.remaining()); UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_DATA, address(buffer)); + return null; } @Override - protected void in(final ByteBuffer buffer, final int size, final Pointer ptr, - final long ptrAddr) { + protected Pointer in(final ByteBuffer buffer, final int size, final Pointer ptr) { + final long ptrAddr = ptr.address(); UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE, size); UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_DATA, address(buffer)); + return null; } @Override - protected ByteBuffer out(final ByteBuffer buffer, final Pointer ptr, - final long ptrAddr) { + protected ByteBuffer out(final ByteBuffer buffer, final Pointer ptr) { + final long ptrAddr = ptr.address(); final long addr = UNSAFE.getLong(ptrAddr + STRUCT_FIELD_OFFSET_DATA); final long size = UNSAFE.getLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE); UNSAFE.putLong(buffer, ADDRESS_OFFSET, addr); @@ -317,5 +336,4 @@ protected ByteBuffer out(final ByteBuffer buffer, final Pointer ptr, return buffer; } } - } diff --git a/src/main/java/org/lmdbjava/ByteUnit.java b/src/main/java/org/lmdbjava/ByteUnit.java new file mode 100644 index 00000000..1c37ad24 --- /dev/null +++ b/src/main/java/org/lmdbjava/ByteUnit.java @@ -0,0 +1,71 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +/** Simple {@link Enum} for converting various IEC and SI byte units down to a number of bytes. */ +public enum ByteUnit { + + /** IEC/SI byte unit for bytes. */ + BYTES(1L), + + /** IEC byte unit for 1024 bytes. */ + KIBIBYTES(1_024L), + /** IEC byte unit for 1024^2 bytes. */ + MEBIBYTES(1_048_576L), + /** IEC byte unit for 1024^3 bytes. */ + GIBIBYTES(1_073_741_824L), + /** IEC byte unit for 1024^4 bytes. */ + TEBIBYTES(1_099_511_627_776L), + /** IEC byte unit for 1024^5 bytes. */ + PEBIBYTES(1_125_899_906_842_624L), + + /** SI byte unit for 1000 bytes. */ + KILOBYTES(1_000L), + /** SI byte unit for 1000^2 bytes. */ + MEGABYTES(1_000_000L), + /** SI byte unit for 1000^3 bytes. */ + GIGABYTES(1_000_000_000L), + /** SI byte unit for 1000^4 bytes. */ + TERABYTES(1_000_000_000_000L), + /** SI byte unit for 1000^5 bytes. */ + PETABYTES(1_000_000_000_000_000L), + ; + + private final long factor; + + ByteUnit(long factor) { + this.factor = factor; + } + + /** + * Convert the value in this byte unit into bytes. + * + * @param value The value to convert. + * @return The number of bytes. + */ + public long toBytes(final long value) { + return value * factor; + } + + /** + * Gets factor to apply when converting this unit into bytes. + * + * @return The factor to apply when converting this unit into bytes. + */ + public long getFactor() { + return factor; + } +} diff --git a/src/main/java/org/lmdbjava/CopyFlagSet.java b/src/main/java/org/lmdbjava/CopyFlagSet.java new file mode 100644 index 00000000..c8e35477 --- /dev/null +++ b/src/main/java/org/lmdbjava/CopyFlagSet.java @@ -0,0 +1,77 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import java.nio.file.Path; +import java.util.Collection; +import java.util.Objects; + +/** An immutable set of flags for use when performing a {@link Env#copy(Path, CopyFlagSet)}. */ +public interface CopyFlagSet extends FlagSet { + + /** An immutable empty {@link CopyFlagSet}. */ + CopyFlagSet EMPTY = CopyFlagSetImpl.EMPTY; + + /** + * Gets the immutable empty {@link CopyFlagSet} instance. + * + * @return The immutable empty {@link CopyFlagSet} instance. + */ + static CopyFlagSet empty() { + return CopyFlagSetImpl.EMPTY; + } + + /** + * Creates an immutable {@link CopyFlagSet} containing copyFlag. + * + * @param copyFlag The flag to include in the {@link CopyFlagSet} + * @return An immutable {@link CopyFlagSet} containing just copyFlag. + */ + static CopyFlagSet of(final CopyFlags copyFlag) { + Objects.requireNonNull(copyFlag); + return copyFlag; + } + + /** + * Creates an immutable {@link CopyFlagSet} containing copyFlags. + * + * @param copyFlags The flags to include in the {@link CopyFlagSet}. + * @return An immutable {@link CopyFlagSet} containing copyFlags. + */ + static CopyFlagSet of(final CopyFlags... copyFlags) { + return builder().setFlags(copyFlags).build(); + } + + /** + * Creates an immutable {@link CopyFlagSet} containing copyFlags. + * + * @param copyFlags The flags to include in the {@link CopyFlagSet}. + * @return An immutable {@link CopyFlagSet} containing copyFlags. + */ + static CopyFlagSet of(final Collection copyFlags) { + return builder().setFlags(copyFlags).build(); + } + + /** + * Create a builder for building an {@link CopyFlagSet}. + * + * @return A builder instance for building an {@link CopyFlagSet}. + */ + static AbstractFlagSet.Builder builder() { + return new AbstractFlagSet.Builder<>( + CopyFlags.class, CopyFlagSetImpl::new, copyFlag -> copyFlag, () -> CopyFlagSetImpl.EMPTY); + } +} diff --git a/src/main/java/org/lmdbjava/CopyFlagSetEmpty.java b/src/main/java/org/lmdbjava/CopyFlagSetEmpty.java new file mode 100644 index 00000000..f18af382 --- /dev/null +++ b/src/main/java/org/lmdbjava/CopyFlagSetEmpty.java @@ -0,0 +1,19 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +class CopyFlagSetEmpty extends AbstractFlagSet.AbstractEmptyFlagSet + implements CopyFlagSet {} diff --git a/src/main/java/org/lmdbjava/CopyFlagSetImpl.java b/src/main/java/org/lmdbjava/CopyFlagSetImpl.java new file mode 100644 index 00000000..a566fc2a --- /dev/null +++ b/src/main/java/org/lmdbjava/CopyFlagSetImpl.java @@ -0,0 +1,27 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import java.util.EnumSet; + +class CopyFlagSetImpl extends AbstractFlagSet implements CopyFlagSet { + + static final CopyFlagSet EMPTY = new CopyFlagSetEmpty(); + + CopyFlagSetImpl(final EnumSet flags) { + super(flags); + } +} diff --git a/src/main/java/org/lmdbjava/CopyFlags.java b/src/main/java/org/lmdbjava/CopyFlags.java index 88649a04..e3677baf 100644 --- a/src/main/java/org/lmdbjava/CopyFlags.java +++ b/src/main/java/org/lmdbjava/CopyFlags.java @@ -1,35 +1,28 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; -/** - * Flags for use when performing a - * {@link Env#copy(java.io.File, org.lmdbjava.CopyFlags...)}. - */ -public enum CopyFlags implements MaskedFlag { +import java.nio.file.Path; +import java.util.EnumSet; +import java.util.Set; + +/** Flags for use when performing a {@link Env#copy(Path, CopyFlagSet)}. */ +public enum CopyFlags implements MaskedFlag, CopyFlagSet { - /** - * Compacting copy: Omit free space from copy, and renumber all pages - * sequentially. - */ + /** Compacting copy: Omit free space from copy, and renumber all pages sequentially. */ MDB_CP_COMPACT(0x01); private final int mask; @@ -43,4 +36,28 @@ public int getMask() { return mask; } + @Override + public Set getFlags() { + return EnumSet.of(this); + } + + @Override + public boolean isSet(final CopyFlags flag) { + return this == flag; + } + + @Override + public int size() { + return 1; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public String toString() { + return FlagSet.asString(this); + } } diff --git a/src/main/java/org/lmdbjava/Cursor.java b/src/main/java/org/lmdbjava/Cursor.java index ed7c1848..0e320930 100644 --- a/src/main/java/org/lmdbjava/Cursor.java +++ b/src/main/java/org/lmdbjava/Cursor.java @@ -1,23 +1,18 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static java.util.Objects.requireNonNull; @@ -25,8 +20,6 @@ import static org.lmdbjava.Dbi.KeyNotFoundException.MDB_NOTFOUND; import static org.lmdbjava.Env.SHOULD_CHECK; import static org.lmdbjava.Library.LIB; -import static org.lmdbjava.MaskedFlag.isSet; -import static org.lmdbjava.MaskedFlag.mask; import static org.lmdbjava.PutFlags.MDB_MULTIPLE; import static org.lmdbjava.PutFlags.MDB_NODUPDATA; import static org.lmdbjava.PutFlags.MDB_NOOVERWRITE; @@ -65,9 +58,8 @@ public final class Cursor implements AutoCloseable { /** * Close a cursor handle. * - *

- * The cursor handle will be freed and must not be used again after this call. - * Its transaction must still be live if it is a write-transaction. + *

The cursor handle will be freed and must not be used again after this call. Its transaction + * must still be live if it is a write-transaction. */ @Override public void close() { @@ -88,9 +80,8 @@ public void close() { /** * Return count of duplicates for current key. * - *

- * This call is only valid on databases that support sorted duplicate data - * items {@link DbiFlags#MDB_DUPSORT}. + *

This call is only valid on databases that support sorted duplicate data items {@link + * DbiFlags#MDB_DUPSORT}. * * @return count of duplicates for current key */ @@ -105,23 +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. + *

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

This function deletes the key/data pair to which the cursor refers. + * + * @param flags flags (either null or {@link PutFlags#MDB_NODUPDATA} */ - public void delete(final PutFlags... f) { + public void delete(final PutFlagSet flags) { if (SHOULD_CHECK) { env.checkNotClosed(); checkNotClosed(); txn.checkReady(); txn.checkWritesAllowed(); } - final int flags = mask(f); - checkRc(LIB.mdb_cursor_del(ptrCursor, flags)); + final PutFlagSet putFlagSet = flags != null ? flags : PutFlagSet.EMPTY; + checkRc(LIB.mdb_cursor_del(ptrCursor, putFlagSet.getMask())); } /** @@ -136,9 +145,9 @@ public boolean first() { /** * Reposition the key/value buffers based on the passed key and operation. * - * @param key to search for + * @param key to search for * @param data to search for - * @param op options for this operation + * @param op options for this operation * @return false if key not found */ public boolean get(final T key, final T data, final SeekOp op) { @@ -149,11 +158,10 @@ public boolean get(final T key, final T data, final SeekOp op) { checkNotClosed(); txn.checkReady(); } - kv.keyIn(key); - kv.valIn(data); + final Pointer transientKey = kv.keyIn(key); + final Pointer transientVal = kv.valIn(data); - final int rc = LIB.mdb_cursor_get(ptrCursor, kv.pointerKey(), kv - .pointerVal(), op.getCode()); + final int rc = LIB.mdb_cursor_get(ptrCursor, kv.pointerKey(), kv.pointerVal(), op.getCode()); if (rc == MDB_NOTFOUND) { return false; @@ -162,6 +170,10 @@ public boolean get(final T key, final T data, final SeekOp op) { checkRc(rc); kv.keyOut(); kv.valOut(); + ReferenceUtil.reachabilityFence0(transientKey); + ReferenceUtil.reachabilityFence0(transientVal); + ReferenceUtil.reachabilityFence0(kv.key()); + ReferenceUtil.reachabilityFence0(kv.val()); ReferenceUtil.reachabilityFence0(key); return true; } @@ -170,7 +182,7 @@ public boolean get(final T key, final T data, final SeekOp op) { * Reposition the key/value buffers based on the passed key and operation. * * @param key to search for - * @param op options for this operation + * @param op options for this operation * @return false if key not found */ public boolean get(final T key, final GetOp op) { @@ -181,10 +193,9 @@ public boolean get(final T key, final GetOp op) { checkNotClosed(); txn.checkReady(); } - kv.keyIn(key); + final Pointer transientKey = kv.keyIn(key); - final int rc = LIB.mdb_cursor_get(ptrCursor, kv.pointerKey(), kv - .pointerVal(), op.getCode()); + final int rc = LIB.mdb_cursor_get(ptrCursor, kv.pointerKey(), kv.pointerVal(), op.getCode()); if (rc == MDB_NOTFOUND) { return false; @@ -193,6 +204,9 @@ public boolean get(final T key, final GetOp op) { checkRc(rc); kv.keyOut(); kv.valOut(); + ReferenceUtil.reachabilityFence0(transientKey); + ReferenceUtil.reachabilityFence0(kv.key()); + ReferenceUtil.reachabilityFence0(kv.val()); ReferenceUtil.reachabilityFence0(key); return true; } @@ -206,6 +220,10 @@ public T key() { return kv.key(); } + KeyVal keyVal() { + return kv; + } + /** * Position at last key/data item. * @@ -233,64 +251,122 @@ public boolean prev() { return seek(MDB_PREV); } + /** + * @deprecated Use {@link Cursor#put(Object, Object, PutFlagSet)} instead.


Store by cursor. + *

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

This function stores key/data pairs into the database. + * + * @param key key to store + * @param val data to store + * @return true if the value was put, false if MDB_NOOVERWRITE or MDB_NODUPDATA were set and the + * key/value existed already. + */ + public boolean put(final T key, final T val) { + return put(key, val, PutFlagSet.EMPTY); + } + /** * Store by cursor. * - *

- * This function stores key/data pairs into the database. + *

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


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

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

- * The database must have been opened with {@link DbiFlags#MDB_DUPFIXED}. The - * buffer must contain fixed-sized values to be inserted. The size of each - * element is calculated from the buffer's size divided by the given element - * count. For example, to populate 10 X 4 byte integers at once, present a - * buffer of 40 bytes and specify the element as 10. + *

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

The database must have been opened with {@link DbiFlags#MDB_DUPFIXED}. The buffer must + * contain fixed-sized values to be inserted. The size of each element is calculated from the + * buffer's size divided by the given element count. For example, to populate 10 X 4 byte integers + * at once, present a buffer of 40 bytes and specify the element as 10. + * + * @param key key to store in the database (not null) + * @param val value to store in the database (not null) + * @param elements number of elements contained in the passed value buffer + * @param flags options for operation (must set MDB_MULTIPLE) Either a {@link + * PutFlagSet} or a single {@link PutFlags}. + */ + public void putMultiple(final T key, final T val, final int elements, final PutFlagSet flags) { if (SHOULD_CHECK) { requireNonNull(txn); requireNonNull(key); @@ -298,16 +374,17 @@ public void putMultiple(final T key, final T val, final int elements, env.checkNotClosed(); txn.checkReady(); txn.checkWritesAllowed(); + if (!flags.isSet(MDB_MULTIPLE)) { + throw new IllegalArgumentException("Must set " + MDB_MULTIPLE + " flag"); + } } - final int mask = mask(op); - if (SHOULD_CHECK && !isSet(mask, MDB_MULTIPLE)) { - throw new IllegalArgumentException("Must set " + MDB_MULTIPLE + " flag"); - } - txn.kv().keyIn(key); + + final Pointer transientKey = txn.kv().keyIn(key); final Pointer dataPtr = txn.kv().valInMulti(val, elements); - final int rc = LIB.mdb_cursor_put(ptrCursor, txn.kv().pointerKey(), - dataPtr, mask); + final int rc = LIB.mdb_cursor_put(ptrCursor, txn.kv().pointerKey(), dataPtr, flags.getMask()); checkRc(rc); + ReferenceUtil.reachabilityFence0(transientKey); + ReferenceUtil.reachabilityFence0(dataPtr); ReferenceUtil.reachabilityFence0(key); ReferenceUtil.reachabilityFence0(val); } @@ -315,13 +392,10 @@ public void putMultiple(final T key, final T val, final int elements, /** * Renew a cursor handle. * - *

- * A cursor is associated with a specific transaction and database. Cursors - * that are only used in read-only transactions may be re-used, to avoid - * unnecessary malloc/free overhead. The cursor may be associated with a new - * read-only transaction, and referencing the same database handle as it was - * created with. This may be done whether the previous transaction is live or - * dead. + *

A cursor is associated with a specific transaction and database. Cursors that are only used + * in read-only transactions may be re-used, to avoid unnecessary malloc/free overhead. The cursor + * may be associated with a new read-only transaction, and referencing the same database handle as + * it was created with. This may be done whether the previous transaction is live or dead. * * @param newTxn transaction handle */ @@ -339,35 +413,71 @@ public void renew(final Txn newTxn) { } /** - * Reserve space for data of the given size, but don't copy the given val. - * Instead, return a pointer to the reserved space, which the caller can fill - * in later - before the next update operation or the transaction ends. This - * saves an extra memcpy if the data is being generated later. LMDB does - * nothing else with this memory, the caller is expected to modify all of the - * space requested. + * @deprecated Use {@link Cursor#reserve(Object, int, PutFlagSet)} instead.


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

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

- * This flag must not be specified if the database was opened with MDB_DUPSORT + *

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

This flag must not be specified if the database was opened with MDB_DUPSORT + * + * @param key key to store in the database (not null) + * @param size size of the value to be stored in the database (not null) + * @param flags options for this operation + * @return a buffer that can be used to modify the value + */ + public T reserve(final T key, final int size, final PutFlagSet flags) { if (SHOULD_CHECK) { requireNonNull(key); + requireNonNull(flags); env.checkNotClosed(); checkNotClosed(); txn.checkReady(); txn.checkWritesAllowed(); } - kv.keyIn(key); - kv.valIn(size); - final int flags = mask(op) | MDB_RESERVE.getMask(); - checkRc(LIB.mdb_cursor_put(ptrCursor, kv.pointerKey(), kv.pointerVal(), - flags)); + final Pointer transientKey = kv.keyIn(key); + final Pointer transientVal = kv.valIn(size); + // This is inconsistent with putMultiple which require MDB_MULTIPLE to be in the set. + final int flagsMask = flags.getMaskWith(MDB_RESERVE); + checkRc(LIB.mdb_cursor_put(ptrCursor, kv.pointerKey(), kv.pointerVal(), flagsMask)); kv.valOut(); + ReferenceUtil.reachabilityFence0(transientKey); + ReferenceUtil.reachabilityFence0(transientVal); ReferenceUtil.reachabilityFence0(key); return val(); } @@ -386,8 +496,7 @@ public boolean seek(final SeekOp op) { txn.checkReady(); } - final int rc = LIB.mdb_cursor_get(ptrCursor, kv.pointerKey(), kv - .pointerVal(), op.getCode()); + final int rc = LIB.mdb_cursor_get(ptrCursor, kv.pointerKey(), kv.pointerVal(), op.getCode()); if (rc == MDB_NOTFOUND) { return false; @@ -414,24 +523,18 @@ private void checkNotClosed() { } } - /** - * Cursor has already been closed. - */ + /** Cursor has already been closed. */ public static final class ClosedException extends LmdbException { private static final long serialVersionUID = 1L; - /** - * Creates a new instance. - */ + /** Creates a new instance. */ public ClosedException() { super("Cursor has already been closed"); } } - /** - * Cursor stack too deep - internal error. - */ + /** Cursor stack too deep - internal error. */ public static final class FullException extends LmdbNativeException { static final int MDB_CURSOR_FULL = -30_787; @@ -441,5 +544,4 @@ public static final class FullException extends LmdbNativeException { super(MDB_CURSOR_FULL, "Cursor stack too deep - internal error"); } } - } diff --git a/src/main/java/org/lmdbjava/CursorIterable.java b/src/main/java/org/lmdbjava/CursorIterable.java index 39a6d58c..65fc1023 100644 --- a/src/main/java/org/lmdbjava/CursorIterable.java +++ b/src/main/java/org/lmdbjava/CursorIterable.java @@ -1,23 +1,18 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static org.lmdbjava.CursorIterable.State.RELEASED; @@ -26,60 +21,74 @@ import static org.lmdbjava.CursorIterable.State.REQUIRES_NEXT_OP; import static org.lmdbjava.CursorIterable.State.TERMINATED; import static org.lmdbjava.GetOp.MDB_SET_RANGE; +import static org.lmdbjava.Library.LIB; import java.util.Comparator; import java.util.Iterator; import java.util.NoSuchElementException; - +import java.util.Objects; +import java.util.function.Supplier; +import jnr.ffi.Pointer; import org.lmdbjava.KeyRangeType.CursorOp; import org.lmdbjava.KeyRangeType.IteratorOp; /** - * {@link Iterable} that creates a single {@link Iterator} that will iterate - * over a {@link Cursor} as specified by a {@link KeyRange}. + * {@link Iterable} that creates a single {@link Iterator} that will iterate over a {@link Cursor} + * as specified by a {@link KeyRange}. * - *

- * An instance will create and close its own cursor. + *

An instance will create and close its own cursor. * * @param buffer type */ -public final class CursorIterable implements - Iterable>, AutoCloseable { +public final class CursorIterable implements Iterable>, AutoCloseable { - private final Comparator comparator; + private final RangeComparator rangeComparator; private final Cursor cursor; private final KeyVal entry; private boolean iteratorReturned; private final KeyRange range; private State state = REQUIRES_INITIAL_OP; - CursorIterable(final Txn txn, final Dbi dbi, final KeyRange range, - final Comparator comparator) { + CursorIterable( + final Txn txn, + final Dbi dbi, + final KeyRange range, + final Comparator comparator, + final BufferProxy proxy) { this.cursor = dbi.openCursor(txn); this.range = range; - this.comparator = comparator; this.entry = new KeyVal<>(); + + if (comparator != null) { + // User supplied Java-side comparator so use that + this.rangeComparator = new JavaRangeComparator<>(range, comparator, cursor::key); + } else { + // No Java-side comparator, so call down to LMDB to do the comparison + this.rangeComparator = new LmdbRangeComparator<>(txn, dbi, cursor, range, proxy); + } } @Override public void close() { cursor.close(); + try { + rangeComparator.close(); + } catch (Exception e) { + throw new RuntimeException(e); + } } /** * Obtain an iterator. * - *

- * As iteration of the returned iterator will cause movement of the underlying - * LMDB cursor, an {@link IllegalStateException} is thrown if an attempt is - * made to obtain the iterator more than once. For advanced cursor control - * (such as being able to iterate over the same data multiple times etc) - * please instead refer to {@link Dbi#openCursor(org.lmdbjava.Txn)}. + *

As iteration of the returned iterator will cause movement of the underlying LMDB cursor, an + * {@link IllegalStateException} is thrown if an attempt is made to obtain the iterator more than + * once. For advanced cursor control (such as being able to iterate over the same data multiple + * times etc) please instead refer to {@link Dbi#openCursor(org.lmdbjava.Txn)}. * * @return an iterator */ @Override - @SuppressWarnings("checkstyle:AnonInnerLength") public Iterator> iterator() { if (iteratorReturned) { throw new IllegalStateException("Iterator can only be returned once"); @@ -106,14 +115,13 @@ public KeyVal next() { @Override public void remove() { - cursor.delete(); + cursor.delete(PutFlags.EMPTY); } }; } - @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NullAssignment"}) private void executeCursorOp(final CursorOp op) { - final boolean found; + boolean found; switch (op) { case FIRST: found = cursor.first(); @@ -131,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"); @@ -141,9 +173,7 @@ private void executeCursorOp(final CursorOp op) { } private void executeIteratorOp() { - final IteratorOp op = range.getType().iteratorOp(range.getStart(), - range.getStop(), - entry.key(), comparator); + final IteratorOp op = range.getType().iteratorOp(entry.key(), rangeComparator); switch (op) { case CALL_NEXT_OP: executeCursorOp(range.getType().nextOp()); @@ -183,10 +213,9 @@ private void update() { /** * Holder for a key and value pair. * - *

- * The same holder instance will always be returned for a given iterator. - * The returned keys and values may change or point to different memory - * locations following changes in the iterator, cursor or transaction. + *

The same holder instance will always be returned for a given iterator. The returned keys and + * values may change or point to different memory locations following changes in the iterator, + * cursor or transaction. * * @param buffer type */ @@ -195,6 +224,9 @@ public static final class KeyVal { private T k; private T v; + /** Explicitly-defined default constructor to avoid warnings. */ + public KeyVal() {} + /** * The key. * @@ -220,15 +252,110 @@ void setK(final T key) { void setV(final T val) { this.v = val; } - } - /** - * Represents the internal {@link CursorIterable} state. - */ + /** Represents the internal {@link CursorIterable} state. */ enum State { - REQUIRES_INITIAL_OP, REQUIRES_NEXT_OP, REQUIRES_ITERATOR_OP, RELEASED, + REQUIRES_INITIAL_OP, + REQUIRES_NEXT_OP, + REQUIRES_ITERATOR_OP, + RELEASED, TERMINATED } + static class JavaRangeComparator implements RangeComparator { + + private final Comparator comparator; + private final Supplier currentKeySupplier; + private final T start; + private final T stop; + + JavaRangeComparator( + final KeyRange range, + final Comparator comparator, + final Supplier currentKeySupplier) { + this.comparator = comparator; + this.currentKeySupplier = currentKeySupplier; + this.start = range.getStart(); + this.stop = range.getStop(); + } + + @Override + public int compareToStartKey() { + return comparator.compare(currentKeySupplier.get(), start); + } + + @Override + public int compareToStopKey() { + return comparator.compare(currentKeySupplier.get(), stop); + } + + @Override + public void close() throws Exception { + // Nothing to close + } + } + + /** + * Calls down to mdb_cmp to make use of the comparator that LMDB uses for insertion order. Has a + * very slight overhead as compared to {@link JavaRangeComparator}. + */ + private static class LmdbRangeComparator implements RangeComparator { + + private final Pointer txnPointer; + private final Pointer dbiPointer; + private final Pointer cursorKeyPointer; + private final Key startKey; + private final Key stopKey; + private final Pointer startKeyPointer; + private final Pointer stopKeyPointer; + + public LmdbRangeComparator( + final Txn txn, + final Dbi dbi, + final Cursor cursor, + final KeyRange range, + final BufferProxy proxy) { + txnPointer = Objects.requireNonNull(txn).pointer(); + dbiPointer = Objects.requireNonNull(dbi).pointer(); + cursorKeyPointer = Objects.requireNonNull(cursor).keyVal().pointerKey(); + // Allocate buffers for use with the start/stop keys if required. + // Saves us copying bytes on each comparison + Objects.requireNonNull(range); + startKey = createKey(range.getStart(), proxy); + stopKey = createKey(range.getStop(), proxy); + startKeyPointer = startKey != null ? startKey.pointer() : null; + stopKeyPointer = stopKey != null ? stopKey.pointer() : null; + } + + @Override + public int compareToStartKey() { + return LIB.mdb_cmp(txnPointer, dbiPointer, cursorKeyPointer, startKeyPointer); + } + + @Override + public int compareToStopKey() { + return LIB.mdb_cmp(txnPointer, dbiPointer, cursorKeyPointer, stopKeyPointer); + } + + @Override + public void close() { + if (startKey != null) { + startKey.close(); + } + if (stopKey != null) { + stopKey.close(); + } + } + + private Key createKey(final T keyBuffer, final BufferProxy proxy) { + if (keyBuffer != null) { + final Key key = proxy.key(); + key.keyIn(keyBuffer); + return key; + } else { + return null; + } + } + } } diff --git a/src/main/java/org/lmdbjava/Dbi.java b/src/main/java/org/lmdbjava/Dbi.java index ef8ec315..d2afdf8f 100644 --- a/src/main/java/org/lmdbjava/Dbi.java +++ b/src/main/java/org/lmdbjava/Dbi.java @@ -1,23 +1,18 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static java.util.Objects.requireNonNull; @@ -36,11 +31,11 @@ import static org.lmdbjava.PutFlags.MDB_RESERVE; import static org.lmdbjava.ResultCodeMapper.checkRc; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; - import jnr.ffi.Pointer; import jnr.ffi.byref.IntByReference; import jnr.ffi.byref.PointerByReference; @@ -54,50 +49,86 @@ */ 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, + final Txn txn, + final byte[] name, + final Comparator comparator, + final boolean nativeCb, + final BufferProxy proxy, + final DbiFlagSet dbiFlagSet) { - Dbi(final Env env, final Txn txn, final byte[] name, - final Comparator comparator, final boolean nativeCb, - final BufferProxy proxy, final DbiFlags... flags) { + if (SHOULD_CHECK) { + if (nativeCb && comparator == null) { + throw new IllegalArgumentException("Is nativeCb is true, you must supply a comparator"); + } + requireNonNull(env); + requireNonNull(txn); + requireNonNull(proxy); + requireNonNull(dbiFlagSet); + txn.checkReady(); + } this.env = env; this.name = name == null ? null : Arrays.copyOf(name, name.length); + this.proxy = proxy; this.comparator = comparator; - final int flagsMask = mask(flags); + 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.allocate(); - final T compKeyB = proxy.allocate(); - proxy.out(compKeyA, keyA, keyA.address()); - proxy.out(compKeyB, keyB, keyB.address()); - 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). * - *

- * It is very rare that closing a database handle is useful. There are also - * many warnings/restrictions if closing a database handle (refer to the LMDB - * C documentation). As such this is non-routine usage and this class does not - * track the open/closed state of the {@link Dbi}. Advanced users are expected - * to have specific reasons for using this method and will manage their own - * state accordingly. + *

It is very rare that closing a database handle is useful. There are also many + * warnings/restrictions if closing a database handle (refer to the LMDB C documentation). As such + * this is non-routine usage and this class does not track the open/closed state of the {@link + * Dbi}. Advanced users are expected to have specific reasons for using this method and will + * manage their own state accordingly. */ public void close() { clean(); @@ -112,7 +143,6 @@ public void close() { * * @param key key to delete from the database (not null) * @return true if the key/data pair was found, false otherwise - * * @see #delete(org.lmdbjava.Txn, java.lang.Object, java.lang.Object) */ public boolean delete(final T key) { @@ -129,7 +159,6 @@ public boolean delete(final T key) { * @param txn transaction handle (not null; not committed; must be R-W) * @param key key to delete from the database (not null) * @return true if the key/data pair was found, false otherwise - * * @see #delete(org.lmdbjava.Txn, java.lang.Object, java.lang.Object) */ public boolean delete(final Txn txn, final T key) { @@ -139,12 +168,10 @@ public boolean delete(final Txn txn, final T key) { /** * Removes key/data pairs from the database. * - *

- * If the database does not support sorted duplicate data items - * ({@link DbiFlags#MDB_DUPSORT}) the value parameter is ignored. If the - * database supports sorted duplicates and the value parameter is null, all of - * the duplicate data items for the key will be deleted. Otherwise, if the - * data parameter is non-null only the matching data item will be deleted. + *

If the database does not support sorted duplicate data items ({@link DbiFlags#MDB_DUPSORT}) + * the value parameter is ignored. If the database supports sorted duplicates and the value + * parameter is null, all of the duplicate data items for the key will be deleted. Otherwise, if + * the data parameter is non-null only the matching data item will be deleted. * * @param txn transaction handle (not null; not committed; must be R-W) * @param key key to delete from the database (not null) @@ -159,11 +186,12 @@ public boolean delete(final Txn txn, final T key, final T val) { txn.checkWritesAllowed(); } - txn.kv().keyIn(key); + final Pointer transientKey = txn.kv().keyIn(key); Pointer data = null; + Pointer transientVal = null; if (val != null) { - txn.kv().valIn(val); + transientVal = txn.kv().valIn(val); data = txn.kv().pointerVal(); } final int rc = LIB.mdb_del(txn.pointer(), ptr, txn.kv().pointerKey(), data); @@ -171,6 +199,8 @@ public boolean delete(final Txn txn, final T key, final T val) { return false; } checkRc(rc); + ReferenceUtil.reachabilityFence0(transientKey); + ReferenceUtil.reachabilityFence0(transientVal); ReferenceUtil.reachabilityFence0(key); ReferenceUtil.reachabilityFence0(val); return true; @@ -179,10 +209,8 @@ public boolean delete(final Txn txn, final T key, final T val) { /** * Drops the data in this database, leaving the database open for further use. * - *

- * This method slightly differs from the LMDB C API in that it does not - * provide support for also closing the DB handle. If closing the DB handle is - * required, please see {@link #close()}. + *

This method slightly differs from the LMDB C API in that it does not provide support for + * also closing the DB handle. If closing the DB handle is required, please see {@link #close()}. * * @param txn transaction handle (not null; not committed; must be R-W) */ @@ -191,11 +219,11 @@ public void drop(final Txn txn) { } /** - * Drops the database. If delete is set to true, the database will be deleted - * and handle will be closed. See {@link #close()} for implication of handle - * close. Otherwise, only the data in this database will be dropped. + * Drops the database. If delete is set to true, the database will be deleted and handle will be + * closed. See {@link #close()} for implication of handle close. Otherwise, only the data in this + * database will be dropped. * - * @param txn transaction handle (not null; not committed; must be R-W) + * @param txn transaction handle (not null; not committed; must be R-W) * @param delete whether database should be deleted. */ public void drop(final Txn txn, final boolean delete) { @@ -215,12 +243,10 @@ public void drop(final Txn txn, final boolean delete) { /** * Get items from a database, moving the {@link Txn#val()} to the value. * - *

- * This function retrieves key/data pairs from the database. The address and - * length of the data associated with the specified \b key are returned in the - * structure to which \b data refers. If the database supports duplicate keys - * ({@link org.lmdbjava.DbiFlags#MDB_DUPSORT}) then the first data item for - * the key will be returned. Retrieval of other items requires the use of + *

This function retrieves key/data pairs from the database. The address and length of the data + * associated with the specified \b key are returned in the structure to which \b data refers. If + * the database supports duplicate keys ({@link org.lmdbjava.DbiFlags#MDB_DUPSORT}) then the first + * data item for the key will be returned. Retrieval of other items requires the use of * #mdb_cursor_get(). * * @param txn transaction handle (not null; not committed) @@ -234,26 +260,72 @@ public T get(final Txn txn, final T key) { env.checkNotClosed(); txn.checkReady(); } - txn.kv().keyIn(key); - final int rc = LIB.mdb_get(txn.pointer(), ptr, txn.kv().pointerKey(), txn - .kv().pointerVal()); + final Pointer transientKey = txn.kv().keyIn(key); + final int rc = LIB.mdb_get(txn.pointer(), ptr, txn.kv().pointerKey(), txn.kv().pointerVal()); if (rc == MDB_NOTFOUND) { return null; } checkRc(rc); + final T result = txn.kv().valOut(); // marked as out in LMDB C docs + ReferenceUtil.reachabilityFence0(transientKey); ReferenceUtil.reachabilityFence0(key); - return txn.kv().valOut(); // marked as out in LMDB C docs + return result; } /** * Obtains the name of this database. * - * @return the name (may be null) + * @return The name (it maybe null) */ public byte[] getName() { return name == null ? null : Arrays.copyOf(name, name.length); } + /** + * 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. * @@ -267,7 +339,7 @@ public CursorIterable iterate(final Txn txn) { /** * Iterate the database in accordance with the provided {@link KeyRange}. * - * @param txn transaction handle (not null; not committed) + * @param txn transaction handle (not null; not committed) * @param range range of acceptable keys (not null) * @return iterator (never null) */ @@ -278,16 +350,17 @@ 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); } - /* - * Return DbiFlags for this Dbi. - * - * @param txn transaction handle (not null; not committed) - * @return the list of flags this Dbi was created with + /** + * Return DbiFlags for this Dbi. + * + * @param txn transaction handle (not null; not committed) + * @return the list of flags this Dbi was created with */ public List listFlags(final Txn txn) { + // TODO we could just return what is in dbiFlagSet, rather than hitting LMDB. if (SHOULD_CHECK) { env.checkNotClosed(); } @@ -310,15 +383,13 @@ public List listFlags(final Txn txn) { /** * Create a cursor handle. * - *

- * A cursor is associated with a specific transaction and database. A cursor - * cannot be used when its database handle is closed. Nor when its transaction - * has ended, except with {@link Cursor#renew(org.lmdbjava.Txn)}. It can be - * discarded with {@link Cursor#close()}. A cursor in a write-transaction can - * be closed before its transaction ends, and will otherwise be closed when - * its transaction ends. A cursor in a read-only transaction must be closed - * explicitly, before or after its transaction ends. It can be reused with - * {@link Cursor#renew(org.lmdbjava.Txn)} before finally closing it. + *

A cursor is associated with a specific transaction and database. A cursor cannot be used + * when its database handle is closed. Nor when its transaction has ended, except with {@link + * Cursor#renew(org.lmdbjava.Txn)}. It can be discarded with {@link Cursor#close()}. A cursor in a + * write-transaction can be closed before its transaction ends, and will otherwise be closed when + * its transaction ends. A cursor in a read-only transaction must be closed explicitly, before or + * after its transaction ends. It can be reused with {@link Cursor#renew(org.lmdbjava.Txn)} before + * finally closing it. * * @param txn transaction handle (not null; not committed) * @return cursor handle @@ -339,80 +410,109 @@ 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. * - *

- * This function stores key/data pairs in the database. The default behavior - * is to enter the new key/data pair, replacing any previously existing key if - * duplicates are disallowed, or adding a duplicate data item if duplicates - * are allowed ({@link DbiFlags#MDB_DUPSORT}). + * @param txn transaction handle (not null; not committed; must be R-W) + * @param key key to store in the database (not null) + * @param val value to store in the database (not null) + * @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. * - * @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. + *

This function stores key/data pairs in the database. The default behavior is to enter the + * new key/data pair, replacing any previously existing key if duplicates are disallowed, or + * adding a duplicate data item if duplicates are allowed ({@link DbiFlags#MDB_DUPSORT}). + * + * @param txn transaction handle (not null; not committed; must be R-W) + * @param key key to store in the database (not null) + * @param val value to store in the database (not null) + * @param flags Special options for this operation. + * @return true if the value was put, false if MDB_NOOVERWRITE or MDB_NODUPDATA were set and the + * key/value existed already. */ - public boolean put(final Txn txn, final T key, final T val, - final PutFlags... flags) { + public boolean put(final Txn txn, final T key, final T val, final PutFlagSet flags) { if (SHOULD_CHECK) { requireNonNull(txn); requireNonNull(key); requireNonNull(val); + requireNonNull(flags); env.checkNotClosed(); txn.checkReady(); txn.checkWritesAllowed(); } - txn.kv().keyIn(key); - txn.kv().valIn(val); - final int mask = mask(flags); - final int rc = LIB.mdb_put(txn.pointer(), ptr, txn.kv().pointerKey(), txn - .kv().pointerVal(), mask); + final Pointer transientKey = txn.kv().keyIn(key); + final Pointer transientVal = txn.kv().valIn(val); + final int rc = + LIB.mdb_put( + txn.pointer(), ptr, txn.kv().pointerKey(), txn.kv().pointerVal(), flags.getMask()); if (rc == MDB_KEYEXIST) { - if (isSet(mask, MDB_NOOVERWRITE)) { + if (flags.isSet(MDB_NOOVERWRITE)) { txn.kv().valOut(); // marked as in,out in LMDB C docs - } else if (!isSet(mask, MDB_NODUPDATA)) { + } else if (!flags.isSet(MDB_NODUPDATA)) { checkRc(rc); } return false; } checkRc(rc); + ReferenceUtil.reachabilityFence0(transientKey); + ReferenceUtil.reachabilityFence0(transientVal); ReferenceUtil.reachabilityFence0(key); ReferenceUtil.reachabilityFence0(val); return true; } /** - * Reserve space for data of the given size, but don't copy the given val. - * Instead, return a pointer to the reserved space, which the caller can fill - * in later - before the next update operation or the transaction ends. This - * saves an extra memcpy if the data is being generated later. LMDB does - * nothing else with this memory, the caller is expected to modify all of the + * Reserve space for data of the given size, but don't copy the given val. Instead, return a + * pointer to the reserved space, which the caller can fill in later - before the next update + * operation or the transaction ends. This saves an extra memcpy if the data is being generated + * later. LMDB does nothing else with this memory, the caller is expected to modify all of the * space requested. * - *

- * This flag must not be specified if the database was opened with MDB_DUPSORT + *

This flag must not be specified if the database was opened with MDB_DUPSORT * - * @param txn transaction handle (not null; not committed; must be R-W) - * @param key key to store in the database (not null) + * @param txn transaction handle (not null; not committed; must be R-W) + * @param key key to store in the database (not null) * @param size size of the value to be stored in the database - * @param op options for this operation + * @param op options for this operation * @return a buffer that can be used to modify the value */ - public T reserve(final Txn txn, final T key, final int size, - final PutFlags... op) { + public T reserve(final Txn txn, final T key, final int size, final PutFlags... op) { if (SHOULD_CHECK) { requireNonNull(txn); requireNonNull(key); @@ -420,12 +520,13 @@ public T reserve(final Txn txn, final T key, final int size, txn.checkReady(); txn.checkWritesAllowed(); } - txn.kv().keyIn(key); - txn.kv().valIn(size); + final Pointer transientKey = txn.kv().keyIn(key); + final Pointer transientVal = txn.kv().valIn(size); final int flags = mask(op) | MDB_RESERVE.getMask(); - checkRc(LIB.mdb_put(txn.pointer(), ptr, txn.kv().pointerKey(), txn.kv() - .pointerVal(), flags)); + checkRc(LIB.mdb_put(txn.pointer(), ptr, txn.kv().pointerKey(), txn.kv().pointerVal(), flags)); txn.kv().valOut(); // marked as in,out in LMDB C docs + ReferenceUtil.reachabilityFence0(transientKey); + ReferenceUtil.reachabilityFence0(transientVal); ReferenceUtil.reachabilityFence0(key); return txn.val(); } @@ -460,9 +561,18 @@ private void clean() { cleaned = true; } - /** - * The specified DBI was changed unexpectedly. - */ + @Override + public String toString() { + String name; + try { + name = getNameAsString(); + } catch (Exception e) { + name = "?"; + } + return "Dbi{" + "name='" + name + "', dbiFlagSet=" + dbiFlagSet + '}'; + } + + /** The specified DBI was changed unexpectedly. */ public static final class BadDbiException extends LmdbNativeException { static final int MDB_BAD_DBI = -30_780; @@ -473,23 +583,18 @@ public static final class BadDbiException extends LmdbNativeException { } } - /** - * Unsupported size of key/DB name/data, or wrong DUPFIXED size. - */ + /** Unsupported size of key/DB name/data, or wrong DUPFIXED size. */ public static final class BadValueSizeException extends LmdbNativeException { static final int MDB_BAD_VALSIZE = -30_781; private static final long serialVersionUID = 1L; BadValueSizeException() { - super(MDB_BAD_VALSIZE, - "Unsupported size of key/DB name/data, or wrong DUPFIXED size"); + super(MDB_BAD_VALSIZE, "Unsupported size of key/DB name/data, or wrong DUPFIXED size"); } } - /** - * Environment maxdbs reached. - */ + /** Environment maxdbs reached. */ public static final class DbFullException extends LmdbNativeException { static final int MDB_DBS_FULL = -30_791; @@ -503,14 +608,13 @@ public static final class DbFullException extends LmdbNativeException { /** * Operation and DB incompatible, or DB type changed. * - *

- * This can mean: + *

This can mean: + * *

    - *
  • The operation expects an MDB_DUPSORT / MDB_DUPFIXED database.
  • - *
  • Opening a named DB when the unnamed DB has MDB_DUPSORT / - * MDB_INTEGERKEY.
  • - *
  • Accessing a data record as a database, or vice versa.
  • - *
  • The database was dropped and recreated with different flags.
  • + *
  • The operation expects an MDB_DUPSORT / MDB_DUPFIXED database. + *
  • Opening a named DB when the unnamed DB has MDB_DUPSORT / MDB_INTEGERKEY. + *
  • Accessing a data record as a database, or vice versa. + *
  • The database was dropped and recreated with different flags. *
*/ public static final class IncompatibleException extends LmdbNativeException { @@ -519,14 +623,11 @@ public static final class IncompatibleException extends LmdbNativeException { private static final long serialVersionUID = 1L; IncompatibleException() { - super(MDB_INCOMPATIBLE, - "Operation and DB incompatible, or DB type changed"); + super(MDB_INCOMPATIBLE, "Operation and DB incompatible, or DB type changed"); } } - /** - * Key/data pair already exists. - */ + /** Key/data pair already exists. */ public static final class KeyExistsException extends LmdbNativeException { static final int MDB_KEYEXIST = -30_799; @@ -537,9 +638,7 @@ public static final class KeyExistsException extends LmdbNativeException { } } - /** - * Key/data pair not found (EOF). - */ + /** Key/data pair not found (EOF). */ public static final class KeyNotFoundException extends LmdbNativeException { static final int MDB_NOTFOUND = -30_798; @@ -550,9 +649,7 @@ public static final class KeyNotFoundException extends LmdbNativeException { } } - /** - * Database contents grew beyond environment mapsize. - */ + /** Database contents grew beyond environment mapsize. */ public static final class MapResizedException extends LmdbNativeException { static final int MDB_MAP_RESIZED = -30_785; diff --git a/src/main/java/org/lmdbjava/DbiBuilder.java b/src/main/java/org/lmdbjava/DbiBuilder.java new file mode 100644 index 00000000..29bbb8f5 --- /dev/null +++ b/src/main/java/org/lmdbjava/DbiBuilder.java @@ -0,0 +1,424 @@ +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.lmdbjava; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Objects; + +/** + * Staged builder for building a {@link Dbi} + * + * @param buffer type + */ +public final class DbiBuilder { + + private final Env env; + private final BufferProxy proxy; + private final boolean readOnly; + private byte[] name; + + DbiBuilder(final Env env, final BufferProxy proxy, final boolean readOnly) { + this.env = Objects.requireNonNull(env); + this.proxy = Objects.requireNonNull(proxy); + this.readOnly = readOnly; + } + + /** + * Create the {@link Dbi} with the passed name. + * + *

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

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

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

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

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

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

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

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

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

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


+ * + *

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

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

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

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

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

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

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

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

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

- * Keys are strings to be compared in reverse order, from the end of the - * strings to the beginning. By default, keys are treated as strings and - * compared from beginning to end. + *

Keys are strings to be compared in reverse order, from the end of the strings to the + * beginning. By default, keys are treated as strings and compared from beginning to end. */ MDB_REVERSEKEY(0x02), /** * 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. + *

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. */ 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), /** * With {@link #MDB_DUPSORT}, sorted dup items have fixed size. * - *

- * This flag may only be used in combination with {@link #MDB_DUPSORT}. This - * option tells the library that the data items for this database are all the - * same size, which allows further optimizations in storage and retrieval. - * When all data items are the same size, the {@link SeekOp#MDB_GET_MULTIPLE} - * and {@link SeekOp#MDB_NEXT_MULTIPLE} cursor operations may be used to + *

This flag may only be used in combination with {@link #MDB_DUPSORT}. This option tells the + * library that the data items for this database are all the same size, which allows further + * optimizations in storage and retrieval. When all data items are the same size, the {@link + * SeekOp#MDB_GET_MULTIPLE} and {@link SeekOp#MDB_NEXT_MULTIPLE} cursor operations may be used to * retrieve multiple items at once. */ MDB_DUPFIXED(0x10), /** * With {@link #MDB_DUPSORT}, dups are {@link #MDB_INTEGERKEY}-style integers. * - *

- * This option specifies that duplicate data items are binary integers, - * similar to {@link #MDB_INTEGERKEY} keys. + *

This option specifies that duplicate data items are binary integers, similar to {@link + * #MDB_INTEGERKEY} keys. */ MDB_INTEGERDUP(0x20), /** * With {@link #MDB_DUPSORT}, use reverse string dups. * - *

- * This option specifies that duplicate data items should be compared as - * strings in reverse order. + *

This option specifies that duplicate data items should be compared as strings in reverse + * order. */ MDB_REVERSEDUP(0x40), /** * Create the named database if it doesn't exist. * - *

- * This option is not allowed in a read-only transaction or a read-only - * environment. + *

This option is not allowed in a read-only transaction or a read-only environment. */ MDB_CREATE(0x4_0000); @@ -96,4 +94,28 @@ public int getMask() { return mask; } + @Override + 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 62b49095..af918943 100644 --- a/src/main/java/org/lmdbjava/DirectBufferProxy.java +++ b/src/main/java/org/lmdbjava/DirectBufferProxy.java @@ -1,23 +1,18 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static java.lang.ThreadLocal.withInitial; @@ -27,9 +22,9 @@ 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; import org.agrona.DirectBuffer; import org.agrona.MutableDirectBuffer; @@ -38,29 +33,27 @@ /** * A buffer proxy backed by Agrona's {@link DirectBuffer}. * - *

- * This class requires {@link UnsafeAccess} and Agrona must be in the classpath. + *

This class requires {@link UnsafeAccess} and Agrona must be in the classpath. */ public final class DirectBufferProxy extends BufferProxy { /** - * The {@link MutableDirectBuffer} proxy. Guaranteed to never be null, - * although a class initialization exception will occur if an attempt is made - * to access this field when unsafe or Agrona is unavailable. + * The {@link MutableDirectBuffer} proxy. Guaranteed to never be null, although a class + * initialization exception will occur if an attempt is made to access this field when unsafe or + * Agrona is unavailable. */ - public static final BufferProxy PROXY_DB - = new DirectBufferProxy(); + public static final BufferProxy PROXY_DB = new DirectBufferProxy(); /** - * A thread-safe pool for a given length. If the buffer found is valid (ie not - * of a negative length) then that buffer is used. If no valid buffer is - * found, a new buffer is created. + * A thread-safe pool for a given length. If the buffer found is valid (ie not of a negative + * length) then that buffer is used. If no valid buffer is found, a new buffer is created. */ - private static final ThreadLocal> BUFFERS - = withInitial(() -> new ArrayDeque<>(16)); + private static final ThreadLocal> BUFFERS = + withInitial(() -> new ArrayDeque<>(16)); - private DirectBufferProxy() { - } + private static final ByteOrder NATIVE_ORDER = ByteOrder.nativeOrder(); + + private DirectBufferProxy() {} /** * Lexicographically compare two buffers. @@ -69,12 +62,10 @@ 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); - if (o1.equals(o2)) { - return 0; - } + final int minLength = Math.min(o1.capacity(), o2.capacity()); final int minWords = minLength / Long.BYTES; @@ -99,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(); @@ -112,8 +144,13 @@ protected DirectBuffer allocate() { } } - protected int compare(final DirectBuffer o1, final DirectBuffer o2) { - return compareBuff(o1, o2); + @Override + public Comparator getComparator(final DbiFlagSet dbiFlagSet) { + if (dbiFlagSet.areAnySet(DbiFlagSet.INTEGER_KEY_FLAGS)) { + return DirectBufferProxy::compareAsIntegerKeys; + } else { + return DirectBufferProxy::compareLexicographically; + } } @Override @@ -130,34 +167,30 @@ protected byte[] getBytes(final DirectBuffer buffer) { } @Override - protected Comparator getComparator(final DbiFlags... flags) { - return this::compare; - } - - @Override - protected void in(final DirectBuffer buffer, final Pointer ptr, - final long ptrAddr) { + protected Pointer in(final DirectBuffer buffer, final Pointer ptr) { + final long ptrAddr = ptr.address(); final long addr = buffer.addressOffset(); final long size = buffer.capacity(); UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_DATA, addr); UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE, size); + return null; } @Override - protected void in(final DirectBuffer buffer, final int size, final Pointer ptr, - final long ptrAddr) { + protected Pointer in(final DirectBuffer buffer, final int size, final Pointer ptr) { + final long ptrAddr = ptr.address(); final long addr = buffer.addressOffset(); UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_DATA, addr); UNSAFE.putLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE, size); + return null; } @Override - protected DirectBuffer out(final DirectBuffer buffer, final Pointer ptr, - final long ptrAddr) { + protected DirectBuffer out(final DirectBuffer buffer, final Pointer ptr) { + final long ptrAddr = ptr.address(); final long addr = UNSAFE.getLong(ptrAddr + STRUCT_FIELD_OFFSET_DATA); final long size = UNSAFE.getLong(ptrAddr + STRUCT_FIELD_OFFSET_SIZE); buffer.wrap(addr, (int) size); return buffer; } - } diff --git a/src/main/java/org/lmdbjava/Env.java b/src/main/java/org/lmdbjava/Env.java index db8b0f4a..4bc6cca8 100644 --- a/src/main/java/org/lmdbjava/Env.java +++ b/src/main/java/org/lmdbjava/Env.java @@ -1,45 +1,43 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static java.lang.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; @@ -51,19 +49,21 @@ * * @param buffer type */ -@SuppressWarnings("PMD.GodClass") 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"; + /** - * Java system property name that can be set to disable optional 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 String DISABLE_CHECKS_PROP = "lmdbjava.disable.checks"; + public static final Charset DEFAULT_NAME_CHARSET = StandardCharsets.UTF_8; /** - * Indicates whether optional checks should be applied in LmdbJava. Optional - * checks are only disabled in critical paths (see package-level JavaDocs). - * Non-critical paths have optional checks performed at all times, regardless - * of this property. + * 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 + * checks performed at all times, regardless of this property. */ public static final boolean SHOULD_CHECK = !getBoolean(DISABLE_CHECKS_PROP); @@ -73,15 +73,24 @@ public final class Env implements AutoCloseable { private final BufferProxy proxy; private final Pointer ptr; private final boolean readOnly; - - private Env(final BufferProxy proxy, final Pointer ptr, - final boolean readOnly, final boolean noSubDir) { + private final Path path; + private final EnvFlagSet envFlagSet; + + private Env( + final BufferProxy proxy, + final Pointer ptr, + final boolean readOnly, + 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; } /** @@ -96,7 +105,7 @@ public static Builder create() { /** * Create an {@link Env} using the passed {@link BufferProxy}. * - * @param buffer type + * @param buffer type * @param proxy the proxy to use (required) * @return the environment (never null) */ @@ -105,26 +114,23 @@ 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 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}. */ - 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); + @Deprecated + public static Env open(final File path, final int size, final EnvFlags... flags) { + return new Builder<>(PROXY_OPTIMAL).setMapSize(size, ByteUnit.MEBIBYTES).open(path, flags); } /** * Close the handle. * - *

- * Will silently return if already closed or never opened. + *

Will silently return if already closed or never opened. */ @Override public void close() { @@ -138,59 +144,119 @@ public void close() { /** * 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. + *

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. + *

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. + *

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 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(flags); - checkRc(LIB.mdb_env_copy2(ptr, path.getAbsolutePath(), flagsMask)); + checkRc(LIB.mdb_env_copy2(ptr, path.toAbsolutePath().toString(), flags.getMask())); } /** * 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 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. + *

This method must not be called from concurrent threads. * * @return a list of DBI names (never null) */ 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()); } /** @@ -199,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. * @@ -249,8 +329,7 @@ public boolean isClosed() { } /** - * Indicates if this environment was opened with - * {@link EnvFlags#MDB_RDONLY_ENV}. + * Indicates if this environment was opened with {@link EnvFlags#MDB_RDONLY_ENV}. * * @return true if read-only */ @@ -259,146 +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}. * - * @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 + *

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 Dbi openDbi(final String name, final DbiFlags... flags) { - final byte[] nameBytes = name == null ? null : name.getBytes(UTF_8); - return openDbi(nameBytes, null, false, flags); + public DbiBuilder createDbi() { + return new DbiBuilder<>(this, proxy, readOnly); } /** - * 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 flags to open the database with + * @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. */ - 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); + @Deprecated() + public Dbi openDbi(final String name, final DbiFlags... 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 may be invoked from native code if - * specified. - * - * @param name name of the database (or null if no name is required) - * @param comparator custom comparator callback (or null to use default) - * @param nativeCb whether native code calls back to the Java comparator - * @param flags to open the database with + * @param name name of the database (or null if no name is required) + * @param comparator custom comparator for cursor start/stop key comparisons. 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) { + return openDbi(Dbi.getNameBytes(name), comparator, false, flags); + } + + /** + * @param name name of the database (or null if no name is required) + * @param comparator custom comparator for cursor start/stop key comparisons 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. */ - 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); + @Deprecated() + public Dbi openDbi( + final String name, + final Comparator comparator, + final boolean nativeCb, + final DbiFlags... 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 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 flags to open the database with + * @param name name of the database (or null if no name is required) + * @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. */ - public Dbi openDbi(final byte[] name, final Comparator comparator, - final DbiFlags... flags) { + @Deprecated() + public Dbi openDbi( + final byte[] name, final Comparator comparator, final DbiFlags... flags) { return openDbi(name, comparator, false, flags); } /** - * 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 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 + * @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 - */ - public Dbi openDbi(final byte[] name, final Comparator comparator, - final boolean nativeCb, final DbiFlags... flags) { + * @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 txn transaction to use (required; not closed) + * @param name name of the database (or null if no name is required) * @param comparator custom comparator callback (or null to use LMDB default) - * @param nativeCb whether native code should call back to the comparator - * @param flags to open the database with + * @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 - */ - public Dbi openDbi(final Txn txn, final byte[] name, - final Comparator comparator, final boolean nativeCb, - final DbiFlags... flags) { - if (SHOULD_CHECK) { - requireNonNull(txn); - txn.checkReady(); - } - final Comparator useComparator; - if (comparator == null) { - useComparator = proxy.getComparator(flags); - } else { - useComparator = comparator; - } - return new Dbi<>(this, txn, name, useComparator, nativeCb, proxy, flags); + * @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, DbiFlagSet.of(flags)); } /** @@ -424,9 +516,8 @@ public Stat stat() { /** * Flushes the data buffers to disk. * - * @param force force a synchronous flush (otherwise if the environment has - * the MDB_NOSYNC flag set the flushes will be omitted, and with - * MDB_MAPASYNC they will be asynchronous) + * @param force force a synchronous flush (otherwise if the environment has the MDB_NOSYNC flag + * set the flushes will be omitted, and with MDB_MAPASYNC they will be asynchronous) */ public void sync(final boolean force) { if (closed) { @@ -436,17 +527,41 @@ public void sync(final boolean force) { checkRc(LIB.mdb_env_sync(ptr, f)); } + /** + * @param parent parent transaction (may be null if no parent) + * @param flags applicable flags (eg for a reusable, read-only transaction) + * @return a transaction (never null) + * @deprecated Instead use {@link Env#txn(Txn, TxnFlagSet)} + *

Obtain a transaction with the requested parent and flags. + */ + @Deprecated + public Txn txn(final Txn parent, final TxnFlags... flags) { + checkNotClosed(); + return new Txn<>(this, parent, proxy, TxnFlagSet.of(flags)); + } + /** * Obtain a transaction with the requested parent and flags. * * @param parent parent transaction (may be null if no parent) - * @param flags applicable flags (eg for a reusable, read-only transaction) * @return a transaction (never null) */ - public Txn txn(final Txn parent, final TxnFlags... flags) { - if (closed) { - throw new AlreadyClosedException(); - } + public Txn txn(final Txn parent) { + checkNotClosed(); + return new Txn<>(this, parent, proxy, TxnFlagSet.EMPTY); + } + + /** + * Obtain a transaction with the requested parent and flags. + * + * @param parent parent transaction (may be null if no parent) + * @param flags applicable flags (e.g. for a reusable, read-only transaction). If the set of flags + * is used frequently it is recommended to hold a static instance of the {@link TxnFlagSet} + * for re-use. + * @return a transaction (never null) + */ + public Txn txn(final Txn parent, final TxnFlagSet flags) { + checkNotClosed(); return new Txn<>(this, parent, proxy, flags); } @@ -456,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); } /** @@ -465,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() { @@ -478,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; @@ -501,46 +618,62 @@ private void validatePath(final File path) { validateDirectoryEmpty(path); } - - /* Check for stale entries in the reader lock table. */ + /** + * Check for stale entries in the reader lock table. + * + * @return 0 on success, non-zero on failure + */ public int readerCheck() { final IntByReference resultPtr = new IntByReference(); checkRc(LIB.mdb_reader_check(ptr, resultPtr)); return resultPtr.intValue(); } - - - /** - * Object has already been closed and the operation is therefore prohibited. - */ + + /** 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 { private static final long serialVersionUID = 1L; - /** - * Creates a new instance. - */ + /** Creates a new instance. */ public AlreadyClosedException() { super("Environment has already been closed"); } } - /** - * Object has already been opened and the operation is therefore prohibited. - */ + /** Object has already been opened and the operation is therefore prohibited. */ public static final class AlreadyOpenException extends LmdbException { private static final long serialVersionUID = 1L; - /** - * Creates a new instance. - */ + /** Creates a new instance. */ public AlreadyOpenException() { super("Environment has already been opened"); } } - /** * Builder for configuring and opening Env. * @@ -549,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); @@ -563,14 +702,54 @@ public static final class Builder { /** * Opens the environment. * - * @param path file system destination - * @param mode Unix permissions to set on created files and semaphores + * @param path file system destination + * @param mode Unix permissions to set on created files and semaphores * @param flags the flags for this new environment * @return an environment ready for use + * @deprecated Instead use {@link Builder#open(Path)}, {@link Builder#setFilePermissions(int)} + * and {@link Builder#setEnvFlags(EnvFlags...)}. + */ + @Deprecated + public Env open(final File path, final int mode, final EnvFlags... flags) { + setFilePermissions(mode); + setEnvFlags(flags); + return open(requireNonNull(path).toPath()); + } + + /** + * Opens the environment. + * + * @param path file system destination + * @return an environment ready for use + * @deprecated Instead use {@link Builder#open(Path)} + */ + @Deprecated + public Env open(final File path) { + return open(requireNonNull(path).toPath()); + } + + /** + * Opens the environment with 0664 mode. + * + * @param path file system destination + * @param flags the flags for this new environment + * @return an environment ready for use + * @deprecated Instead use {@link Builder#open(Path)} and {@link + * Builder#setEnvFlags(EnvFlags...)}. + */ + @Deprecated + public Env open(final File path, final EnvFlags... flags) { + setEnvFlags(flags); + return open(requireNonNull(path).toPath()); + } + + /** + * Opens the environment. + * + * @param path file system destination + * @return an environment ready for use */ - @SuppressWarnings("PMD.AccessorClassGeneration") - public Env open(final File path, final int mode, - final EnvFlags... flags) { + public Env open(final Path path) { requireNonNull(path); if (opened) { throw new AlreadyOpenException(); @@ -583,11 +762,11 @@ public Env open(final File path, final int mode, 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(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; @@ -595,20 +774,7 @@ public Env open(final File path, final int mode, } /** - * 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 - */ - @SuppressWarnings("PMD.AvoidUsingOctalValues") - 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 @@ -624,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. * @@ -651,11 +832,107 @@ 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. - */ + /** File is not a valid LMDB file. */ public static final class FileInvalidException extends LmdbNativeException { static final int MDB_INVALID = -30_793; @@ -666,9 +943,7 @@ public static final class FileInvalidException extends LmdbNativeException { } } - /** - * The specified copy destination is invalid. - */ + /** The specified copy destination is invalid. */ public static final class InvalidCopyDestination extends LmdbException { private static final long serialVersionUID = 1L; @@ -683,9 +958,7 @@ public InvalidCopyDestination(final String message) { } } - /** - * Environment mapsize reached. - */ + /** Environment mapsize reached. */ public static final class MapFullException extends LmdbNativeException { static final int MDB_MAP_FULL = -30_792; @@ -696,9 +969,7 @@ public static final class MapFullException extends LmdbNativeException { } } - /** - * Environment maxreaders reached. - */ + /** Environment maxreaders reached. */ public static final class ReadersFullException extends LmdbNativeException { static final int MDB_READERS_FULL = -30_790; @@ -709,9 +980,7 @@ public static final class ReadersFullException extends LmdbNativeException { } } - /** - * Environment version mismatch. - */ + /** Environment version mismatch. */ public static final class VersionMismatchException extends LmdbNativeException { static final int MDB_VERSION_MISMATCH = -30_794; @@ -721,5 +990,4 @@ public static final class VersionMismatchException extends LmdbNativeException { super(MDB_VERSION_MISMATCH, "Environment version mismatch"); } } - } 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 ab54917a..1d5c9214 100644 --- a/src/main/java/org/lmdbjava/EnvFlags.java +++ b/src/main/java/org/lmdbjava/EnvFlags.java @@ -1,169 +1,139 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; -/** - * Flags for use when opening the {@link Env}. - */ -public enum EnvFlags implements MaskedFlag { +import java.util.EnumSet; +import java.util.Set; + +/** Flags for use when opening the {@link Env}. */ +public enum EnvFlags implements MaskedFlag, EnvFlagSet { /** * Mmap at a fixed address (experimental). * - *

- * Use a fixed address for the mmap region. This flag must be specified when - * creating the environment, and is stored persistently in the environment. If - * successful, the memory map will always reside at the same virtual address - * and pointers used to reference data items in the database will be constant - * across multiple invocations. This option may not always work, depending on - * how the operating system has allocated memory to shared libraries and other - * uses. The feature is highly experimental. + *

Use a fixed address for the mmap region. This flag must be specified when creating the + * environment, and is stored persistently in the environment. If successful, the memory map will + * always reside at the same virtual address and pointers used to reference data items in the + * database will be constant across multiple invocations. This option may not always work, + * depending on how the operating system has allocated memory to shared libraries and other uses. + * The feature is highly experimental. */ MDB_FIXEDMAP(0x01), /** * No environment directory. * - *

- * By default, LMDB creates its environment in a directory whose pathname is - * given in path, and creates its data and lock files under that directory. - * With this option, path is used as-is for the database main data file. The - * database lock file is the path with "-lock" appended. + *

By default, LMDB creates its environment in a directory whose pathname is given in path, and + * creates its data and lock files under that directory. With this option, path is used as-is for + * the database main data file. The database lock file is the path with "-lock" appended. */ MDB_NOSUBDIR(0x4000), /** * Open the environment in read-only mode. * - *

- * No write operations will be allowed. LMDB will still modify the lock file - - * except on read-only filesystems, where LMDB does not use locks. + *

No write operations will be allowed. LMDB will still modify the lock file - except on + * read-only filesystems, where LMDB does not use locks. */ MDB_RDONLY_ENV(0x2_0000), /** * Use a writeable memory map unless {@link #MDB_RDONLY_ENV} is set. * - *

- * This is faster and uses fewer mallocs, but loses protection from - * application bugs like wild pointer writes and other bad updates into the - * database. Incompatible with nested transactions. Do not mix processes with - * and without {@link #MDB_WRITEMAP} on the same environment. This can defeat - * durability ({@link Env#sync(boolean)} etc). + *

This is faster and uses fewer mallocs, but loses protection from application bugs like wild + * pointer writes and other bad updates into the database. Incompatible with nested transactions. + * Do not mix processes with and without {@link #MDB_WRITEMAP} on the same environment. This can + * defeat durability ({@link Env#sync(boolean)} etc). */ MDB_WRITEMAP(0x8_0000), /** * Don't fsync metapage after commit. * - *

- * Flush system buffers to disk only once per transaction, omit the metadata - * flush. Defer that until the system flushes files to disk, or next - * non-{@link #MDB_RDONLY_ENV} commit or {@link Env#sync(boolean)}. This - * optimization* maintains database integrity, but a system crash may undo the - * last* committed transaction. I.e. it preserves the ACI (atomicity, - * consistency, isolation) but not D (durability) database property. + *

Flush system buffers to disk only once per transaction, omit the metadata flush. Defer that + * until the system flushes files to disk, or next non-{@link #MDB_RDONLY_ENV} commit or {@link + * Env#sync(boolean)}. This optimization* maintains database integrity, but a system crash may + * undo the last* committed transaction. I.e. it preserves the ACI (atomicity, consistency, + * isolation) but not D (durability) database property. */ MDB_NOMETASYNC(0x4_0000), /** * Don't fsync after commit. * - *

- * Don't flush system buffers to disk when committing a transaction. This - * optimization means a system crash can corrupt the database or lose the last - * transactions if buffers are not yet flushed to disk. The risk is governed - * by how often the system flushes dirty buffers to disk and how often - * {@link Env#sync(boolean)} is called. However, if the filesystem preserves - * write order and the {@link #MDB_WRITEMAP} flag is not used, transactions - * exhibit ACI (atomicity, consistency, isolation) properties and only lose D - * (durability). I.e. database integrity is maintained, but a system crash may - * undo the final transactions. Note that - * ({@link #MDB_NOSYNC} | {@link #MDB_WRITEMAP}) leaves the system with no - * hint for when to write transactions to disk, unless - * {@link Env#sync(boolean)} is called. - * ({@link #MDB_MAPASYNC} | {@link #MDB_WRITEMAP}) may be preferable. + *

Don't flush system buffers to disk when committing a transaction. This optimization means a + * system crash can corrupt the database or lose the last transactions if buffers are not yet + * flushed to disk. The risk is governed by how often the system flushes dirty buffers to disk and + * how often {@link Env#sync(boolean)} is called. However, if the filesystem preserves write order + * and the {@link #MDB_WRITEMAP} flag is not used, transactions exhibit ACI (atomicity, + * consistency, isolation) properties and only lose D (durability). I.e. database integrity is + * maintained, but a system crash may undo the final transactions. Note that ({@link #MDB_NOSYNC} + * | {@link #MDB_WRITEMAP}) leaves the system with no hint for when to write transactions to disk, + * unless {@link Env#sync(boolean)} is called. ({@link #MDB_MAPASYNC} | {@link #MDB_WRITEMAP}) may + * be preferable. */ MDB_NOSYNC(0x1_0000), /** * Use asynchronous msync when {@link #MDB_WRITEMAP} is used. * - *

- * When using {@link #MDB_WRITEMAP}, use asynchronous flushes to disk. - * As with {@link #MDB_NOSYNC}, a system crash can then corrupt the database - * or lose the last transactions. Calling {@link Env#sync(boolean)} ensures - * on-disk database integrity until next commit. + *

When using {@link #MDB_WRITEMAP}, use asynchronous flushes to disk. As with {@link + * #MDB_NOSYNC}, a system crash can then corrupt the database or lose the last transactions. + * Calling {@link Env#sync(boolean)} ensures on-disk database integrity until next commit. */ MDB_MAPASYNC(0x10_0000), /** * Tie reader locktable slots to {@link Txn} objects instead of to threads. * - *

- * Don't use Thread-Local Storage. Tie reader locktable slots to {@link Txn} - * objects instead of to threads. I.e. {@link Txn#reset()} keeps the slot - * reseved for the {@link Txn} object. A thread may use parallel read-only - * transactions. A read-only transaction may span threads if the user - * synchronizes its use. Applications that multiplex many user threads over - * individual OS threads need this option. Such an application must also - * serialize the write transactions in an OS thread, since LMDB's write - * locking is unaware of the user threads. + *

Don't use Thread-Local Storage. Tie reader locktable slots to {@link Txn} objects instead of + * to threads. I.e. {@link Txn#reset()} keeps the slot reseved for the {@link Txn} object. A + * thread may use parallel read-only transactions. A read-only transaction may span threads if the + * user synchronizes its use. Applications that multiplex many user threads over individual OS + * threads need this option. Such an application must also serialize the write transactions in an + * OS thread, since LMDB's write locking is unaware of the user threads. */ MDB_NOTLS(0x20_0000), /** * Don't do any locking, caller must manage their own locks. * - *

- * Don't do any locking. If concurrent access is anticipated, the caller must - * manage all concurrency itself. For proper operation the caller must enforce - * single-writer semantics, and must ensure that no readers are using old - * transactions while a writer is active. The simplest approach is to use an - * exclusive lock so that no readers may be active at all when a writer + *

Don't do any locking. If concurrent access is anticipated, the caller must manage all + * concurrency itself. For proper operation the caller must enforce single-writer semantics, and + * must ensure that no readers are using old transactions while a writer is active. The simplest + * approach is to use an exclusive lock so that no readers may be active at all when a writer * begins. */ MDB_NOLOCK(0x40_0000), /** * Don't do readahead (no effect on Windows). * - *

- * Turn off readahead. Most operating systems perform readahead on read - * requests by default. This option turns it off if the OS supports it. - * Turning it off may help random read performance when the DB is larger than - * RAM and system RAM is full. The option is not implemented on Windows. + *

Turn off readahead. Most operating systems perform readahead on read requests by default. + * This option turns it off if the OS supports it. Turning it off may help random read performance + * when the DB is larger than RAM and system RAM is full. The option is not implemented on + * Windows. */ MDB_NORDAHEAD(0x80_0000), /** * Don't initialize malloc'd memory before writing to datafile. * - *

- * Don't initialize malloc'd memory before writing to unused spaces in the - * data file. By default, memory for pages written to the data file is - * obtained using malloc. While these pages may be reused in subsequent - * transactions, freshly malloc'd pages will be initialized to zeroes before - * use. This avoids persisting leftover data from other code (that used the - * heap and subsequently freed the memory) into the data file. Note that many - * other system libraries may allocate and free memory from the heap for - * arbitrary uses. E.g., stdio may use the heap for file I/O buffers. This - * initialization step has a modest performance cost so some applications may - * want to disable it using this flag. This option can be a problem for - * applications which handle sensitive data like passwords, and it makes - * memory checkers like Valgrind noisy. This flag is not needed with - * {@link #MDB_WRITEMAP}, which writes directly to the mmap instead of using - * malloc for pages. The initialization is also skipped if - * {@link PutFlags#MDB_RESERVE} is used; the caller is expected to overwrite - * all of the memory that was reserved in that case. + *

Don't initialize malloc'd memory before writing to unused spaces in the data file. By + * default, memory for pages written to the data file is obtained using malloc. While these pages + * may be reused in subsequent transactions, freshly malloc'd pages will be initialized to zeroes + * before use. This avoids persisting leftover data from other code (that used the heap and + * subsequently freed the memory) into the data file. Note that many other system libraries may + * allocate and free memory from the heap for arbitrary uses. E.g., stdio may use the heap for + * file I/O buffers. This initialization step has a modest performance cost so some applications + * may want to disable it using this flag. This option can be a problem for applications which + * handle sensitive data like passwords, and it makes memory checkers like Valgrind noisy. This + * flag is not needed with {@link #MDB_WRITEMAP}, which writes directly to the mmap instead of + * using malloc for pages. The initialization is also skipped if {@link PutFlags#MDB_RESERVE} is + * used; the caller is expected to overwrite all of the memory that was reserved in that case. */ MDB_NOMEMINIT(0x100_0000); @@ -178,4 +148,28 @@ 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/main/java/org/lmdbjava/EnvInfo.java b/src/main/java/org/lmdbjava/EnvInfo.java index a1ae62ba..71169d90 100644 --- a/src/main/java/org/lmdbjava/EnvInfo.java +++ b/src/main/java/org/lmdbjava/EnvInfo.java @@ -1,63 +1,48 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; -/** - * Environment information, as returned by {@link Env#info()}. - */ +/** Environment information, as returned by {@link Env#info()}. */ public final class EnvInfo { - /** - * ID of the last used page. - */ + /** ID of the last used page. */ public final long lastPageNumber; - /** - * ID of the last committed transaction. - */ + /** ID of the last committed transaction. */ public final long lastTransactionId; - /** - * Address of map, if fixed. - */ + /** Address of map, if fixed. */ public final long mapAddress; - /** - * Size of the data memory map. - */ + /** Size of the data memory map. */ public final long mapSize; - /** - * Max reader slots in the environment. - */ + /** Max reader slots in the environment. */ public final int maxReaders; - /** - * Max reader slots used in the environment. - */ + /** Max reader slots used in the environment. */ public final int numReaders; - EnvInfo(final long mapAddress, final long mapSize, final long lastPageNumber, - final long lastTransactionId, final int maxReaders, - final int numReaders) { + EnvInfo( + final long mapAddress, + final long mapSize, + final long lastPageNumber, + final long lastTransactionId, + final int maxReaders, + final int numReaders) { this.mapAddress = mapAddress; this.mapSize = mapSize; this.lastPageNumber = lastPageNumber; @@ -68,10 +53,19 @@ public final class EnvInfo { @Override public String toString() { - return "EnvInfo{" + "lastPageNumber=" + lastPageNumber - + ", lastTransactionId=" + lastTransactionId + ", mapAddress=" - + mapAddress + ", mapSize=" + mapSize + ", maxReaders=" - + maxReaders + ", numReaders=" + numReaders + '}'; + return "EnvInfo{" + + "lastPageNumber=" + + lastPageNumber + + ", lastTransactionId=" + + lastTransactionId + + ", mapAddress=" + + mapAddress + + ", mapSize=" + + mapSize + + ", maxReaders=" + + maxReaders + + ", numReaders=" + + numReaders + + '}'; } - } diff --git a/src/main/java/org/lmdbjava/FileUtil.java b/src/main/java/org/lmdbjava/FileUtil.java new file mode 100644 index 00000000..476f0436 --- /dev/null +++ b/src/main/java/org/lmdbjava/FileUtil.java @@ -0,0 +1,112 @@ +/* + * 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.FileVisitOption; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.NotDirectoryException; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.EnumSet; +import java.util.stream.Stream; + +final class FileUtil { + + private FileUtil() {} + + static long size(final Path path) { + try { + return Files.size(path); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + } + + static void deleteFile(final Path path) { + try { + Files.delete(path); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + } + + static void deleteDir(final Path path) { + if (path != null && Files.isDirectory(path)) { + recursiveDelete(path); + deleteIfExists(path); + } + } + + private static void recursiveDelete(final Path path) { + try { + Files.walkFileTree( + path, + EnumSet.of(FileVisitOption.FOLLOW_LINKS), + Integer.MAX_VALUE, + new FileVisitor() { + @Override + public FileVisitResult preVisitDirectory( + final Path dir, final BasicFileAttributes attrs) throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(final Path file, final IOException exc) + throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) { + deleteIfExists(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) { + if (!dir.equals(path)) { + deleteIfExists(dir); + } + return FileVisitResult.CONTINUE; + } + }); + } catch (final NotDirectoryException e) { + // Ignore. + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + } + + private static void deleteIfExists(final Path path) { + try { + Files.deleteIfExists(path); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + } + + static long count(final Path path) { + try (final Stream stream = Files.list(path)) { + return stream.count(); + } catch (final IOException e) { + throw new UncheckedIOException(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/GetOp.java b/src/main/java/org/lmdbjava/GetOp.java index 9d36c204..666f692c 100644 --- a/src/main/java/org/lmdbjava/GetOp.java +++ b/src/main/java/org/lmdbjava/GetOp.java @@ -1,45 +1,32 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; /** - * Flags for use when performing a - * {@link Cursor#get(java.lang.Object, org.lmdbjava.GetOp)}. + * Flags for use when performing a {@link Cursor#get(java.lang.Object, org.lmdbjava.GetOp)}. * - *

- * Unlike most other LMDB enums, this enum is not bit masked. + *

Unlike most other LMDB enums, this enum is not bit masked. */ public enum GetOp { - /** - * Position at specified key. - */ + /** Position at specified key. */ MDB_SET(15), - /** - * Position at specified key, return key + data. - */ + /** Position at specified key, return key + data. */ MDB_SET_KEY(16), - /** - * Position at first key greater than or equal to specified key. - */ + /** Position at first key greater than or equal to specified key. */ MDB_SET_RANGE(17); private final int code; @@ -56,5 +43,4 @@ public enum GetOp { public int getCode() { return code; } - } 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/KeyRange.java b/src/main/java/org/lmdbjava/KeyRange.java index d47c444c..553346e6 100644 --- a/src/main/java/org/lmdbjava/KeyRange.java +++ b/src/main/java/org/lmdbjava/KeyRange.java @@ -1,23 +1,18 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static java.util.Objects.requireNonNull; @@ -27,8 +22,7 @@ /** * Limits the range and direction of keys to iterate. * - *

- * Immutable once created (although the buffers themselves may not be). + *

Immutable once created (although the buffers themselves may not be). * * @param buffer type */ @@ -43,13 +37,12 @@ public final class KeyRange { /** * Construct a key range. * - *

- * End user code may find it more expressive to use one of the static methods - * provided on this class. + *

End user code may find it more expressive to use one of the static methods provided on this + * class. * - * @param type key type + * @param type key type * @param start start key (required if applicable for the passed range type) - * @param stop stop key (required if applicable for the passed range type) + * @param stop stop key (required if applicable for the passed range type) */ public KeyRange(final KeyRangeType type, final T start, final T stop) { requireNonNull(type, "Key range type is required"); @@ -87,7 +80,7 @@ public static KeyRange allBackward() { /** * Create a {@link KeyRangeType#FORWARD_AT_LEAST} range. * - * @param buffer type + * @param buffer type * @param start start key (required) * @return a key range (never null) */ @@ -98,7 +91,7 @@ public static KeyRange atLeast(final T start) { /** * Create a {@link KeyRangeType#BACKWARD_AT_LEAST} range. * - * @param buffer type + * @param buffer type * @param start start key (required) * @return a key range (never null) */ @@ -109,7 +102,7 @@ public static KeyRange atLeastBackward(final T start) { /** * Create a {@link KeyRangeType#FORWARD_AT_MOST} range. * - * @param buffer type + * @param buffer type * @param stop stop key (required) * @return a key range (never null) */ @@ -120,7 +113,7 @@ public static KeyRange atMost(final T stop) { /** * Create a {@link KeyRangeType#BACKWARD_AT_MOST} range. * - * @param buffer type + * @param buffer type * @param stop stop key (required) * @return a key range (never null) */ @@ -131,9 +124,9 @@ public static KeyRange atMostBackward(final T stop) { /** * Create a {@link KeyRangeType#FORWARD_CLOSED} range. * - * @param buffer type + * @param buffer type * @param start start key (required) - * @param stop stop key (required) + * @param stop stop key (required) * @return a key range (never null) */ public static KeyRange closed(final T start, final T stop) { @@ -143,9 +136,9 @@ public static KeyRange closed(final T start, final T stop) { /** * Create a {@link KeyRangeType#BACKWARD_CLOSED} range. * - * @param buffer type + * @param buffer type * @param start start key (required) - * @param stop stop key (required) + * @param stop stop key (required) * @return a key range (never null) */ public static KeyRange closedBackward(final T start, final T stop) { @@ -155,9 +148,9 @@ public static KeyRange closedBackward(final T start, final T stop) { /** * Create a {@link KeyRangeType#FORWARD_CLOSED_OPEN} range. * - * @param buffer type + * @param buffer type * @param start start key (required) - * @param stop stop key (required) + * @param stop stop key (required) * @return a key range (never null) */ public static KeyRange closedOpen(final T start, final T stop) { @@ -167,9 +160,9 @@ public static KeyRange closedOpen(final T start, final T stop) { /** * Create a {@link KeyRangeType#BACKWARD_CLOSED_OPEN} range. * - * @param buffer type + * @param buffer type * @param start start key (required) - * @param stop stop key (required) + * @param stop stop key (required) * @return a key range (never null) */ public static KeyRange closedOpenBackward(final T start, final T stop) { @@ -179,7 +172,7 @@ public static KeyRange closedOpenBackward(final T start, final T stop) { /** * Create a {@link KeyRangeType#FORWARD_GREATER_THAN} range. * - * @param buffer type + * @param buffer type * @param start start key (required) * @return a key range (never null) */ @@ -190,7 +183,7 @@ public static KeyRange greaterThan(final T start) { /** * Create a {@link KeyRangeType#BACKWARD_GREATER_THAN} range. * - * @param buffer type + * @param buffer type * @param start start key (required) * @return a key range (never null) */ @@ -201,7 +194,7 @@ public static KeyRange greaterThanBackward(final T start) { /** * Create a {@link KeyRangeType#FORWARD_LESS_THAN} range. * - * @param buffer type + * @param buffer type * @param stop stop key (required) * @return a key range (never null) */ @@ -212,7 +205,7 @@ public static KeyRange lessThan(final T stop) { /** * Create a {@link KeyRangeType#BACKWARD_LESS_THAN} range. * - * @param buffer type + * @param buffer type * @param stop stop key (required) * @return a key range (never null) */ @@ -223,9 +216,9 @@ public static KeyRange lessThanBackward(final T stop) { /** * Create a {@link KeyRangeType#FORWARD_OPEN} range. * - * @param buffer type + * @param buffer type * @param start start key (required) - * @param stop stop key (required) + * @param stop stop key (required) * @return a key range (never null) */ public static KeyRange open(final T start, final T stop) { @@ -235,9 +228,9 @@ public static KeyRange open(final T start, final T stop) { /** * Create a {@link KeyRangeType#BACKWARD_OPEN} range. * - * @param buffer type + * @param buffer type * @param start start key (required) - * @param stop stop key (required) + * @param stop stop key (required) * @return a key range (never null) */ public static KeyRange openBackward(final T start, final T stop) { @@ -247,9 +240,9 @@ public static KeyRange openBackward(final T start, final T stop) { /** * Create a {@link KeyRangeType#FORWARD_OPEN_CLOSED} range. * - * @param buffer type + * @param buffer type * @param start start key (required) - * @param stop stop key (required) + * @param stop stop key (required) * @return a key range (never null) */ public static KeyRange openClosed(final T start, final T stop) { @@ -259,9 +252,9 @@ public static KeyRange openClosed(final T start, final T stop) { /** * Create a {@link KeyRangeType#BACKWARD_OPEN_CLOSED} range. * - * @param buffer type + * @param buffer type * @param start start key (required) - * @param stop stop key (required) + * @param stop stop key (required) * @return a key range (never null) */ public static KeyRange openClosedBackward(final T start, final T stop) { @@ -294,5 +287,4 @@ public T getStop() { public KeyRangeType getType() { return type; } - } diff --git a/src/main/java/org/lmdbjava/KeyRangeType.java b/src/main/java/org/lmdbjava/KeyRangeType.java index 267e45c8..4cb1cc9b 100644 --- a/src/main/java/org/lmdbjava/KeyRangeType.java +++ b/src/main/java/org/lmdbjava/KeyRangeType.java @@ -1,23 +1,18 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static java.util.Objects.requireNonNull; @@ -36,253 +31,215 @@ /** * Key range type. * - *

- * The terminology used in this class is adapted from Google Guava's ranges. - * Refer to the - * Ranges Explained wiki page for more information. LmddJava prepends either - * "FORWARD" or "BACKWARD" to denote the iterator order. + *

The terminology used in this class is adapted from Google Guava's ranges. Refer to the Ranges Explained wiki page for + * more information. LmddJava prepends either "FORWARD" or "BACKWARD" to denote the iterator order. * - *

- * In the examples below, it is assumed the table has keys 2, 4, 6 and 8. + *

In the examples below, it is assumed the table has keys 2, 4, 6 and 8. */ -@SuppressWarnings("PMD.CyclomaticComplexity") public enum KeyRangeType { /** * Starting on the first key and iterate forward until no keys remain. * - *

- * The "start" and "stop" values are ignored. + *

The "start" and "stop" values are ignored. * - *

- * In our example, the returned keys would be 2, 4, 6 and 8. + *

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. + * Start on the passed key (or the first key immediately after it) and iterate forward until no + * keys remain. * - *

- * The "start" value is required. The "stop" value is ignored. + *

The "start" value is required. The "stop" value is ignored. * - *

- * 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. + *

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. + * Start on the first key and iterate forward until a key equal to it (or the first key + * immediately after it) is reached. * - *

- * The "stop" value is required. The "start" value is ignored. + *

The "stop" value is required. The "start" value is ignored. * - *

- * 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. + *

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 preceding it in the case of the "stop" - * key). + * 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 + * preceding it in the case of the "stop" key). * - *

- * The "start" and "stop" values are both required. + *

The "start" and "stop" values are both required. * - *

- * 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. + *

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 preceding it in the case of the "stop" - * key). Do not return the "stop" key. + * 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 + * preceding it in the case of the "stop" key). Do not return the "stop" key. * - *

- * The "start" and "stop" values are both required. + *

The "start" and "stop" values are both required. * - *

- * 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. + *

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. + * Start after the passed key (but not equal to it) and iterate forward until no keys remain. * - *

- * The "start" value is required. The "stop" value is ignored. + *

The "start" value is required. The "stop" value is ignored. * - *

- * 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. + *

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). + * Start on the first key and iterate forward until a key the passed key has been reached (but do + * not return that key). * - *

- * The "stop" value is required. The "start" value is ignored. + *

The "stop" value is required. The "start" value is ignored. * - *

- * 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. + *

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. * - *

- * The "start" and "stop" values are both required. + *

The "start" and "stop" values are both required. * - *

- * 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. + *

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. + * Iterate forward between the passed keys. Do not return the "start" key, but do return the + * "stop" key. * - *

- * The "start" and "stop" values are both required. + *

The "start" and "stop" values are both required. * - *

- * 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. + *

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. * - *

- * The "start" and "stop" values are ignored. + *

The "start" and "stop" values are ignored. * - *

- * In our example, the returned keys would be 8, 6, 4 and 2. + *

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. + * Start on the passed key (or the first key immediately preceding it) and iterate backward until + * no keys remain. * - *

- * The "start" value is required. The "stop" value is ignored. + *

The "start" value is required. The "stop" value is ignored. * - *

- * In our example and with a passed search key of 5, the returned keys would - * be 4 and 2. With a 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. + *

In our example and with a passed search key of 5, the returned keys would be 4 and 2. With a + * 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. + * Start on the last key and iterate backward until a key equal to it (or the first key + * immediately preceding it it) is reached. * - *

- * The "stop" value is required. The "start" value is ignored. + *

The "stop" value is required. The "start" value is ignored. * - *

- * 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. + *

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 following it in the case of the "stop" - * key). - * - *

- * The "start" and "stop" values are both required. - * - *

- * In our example and with a passed search range of 7 - 3, the returned keys - * would be 6 and 4. 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. + * 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 + * following it in the case of the "stop" key). + * + *

The "start" and "stop" values are both required. + * + *

In our example and with a passed search range of 7 - 3, the returned keys would be 6 and 4. + * 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 following it in the case of the "stop" - * key). Do not return the "stop" key. - * - *

- * The "start" and "stop" values are both required. - * - *

- * In our example and with a passed search range of 8 - 3, the returned keys - * would be 8, 6 and 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. + * 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 + * following it in the case of the "stop" key). Do not return the "stop" key. + * + *

The "start" and "stop" values are both required. + * + *

In our example and with a passed search range of 8 - 3, the returned keys would be 8, 6 and + * 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. + * Start immediate prior to the passed key (but not equal to it) and iterate backward until no + * keys remain. * - *

- * The "start" value is required. The "stop" value is ignored. + *

The "start" value is required. The "stop" value is ignored. * - *

- * In our example and with a passed search key of 6, the returned keys would - * be 4 and 2. With a 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. + *

In our example and with a passed search key of 6, the returned keys would be 4 and 2. With a + * 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. + * 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. * - *

- * The "stop" value is required. The "start" value is ignored. + *

The "stop" value is required. The "start" value is ignored. * - *

- * 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 + *

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. + * Iterate backward between the passed keys, but do not return the passed keys. * - *

- * The "start" and "stop" values are both required. + *

The "start" and "stop" values are both required. * - *

- * In our example and with a passed search range of 7 - 2, the returned keys - * would be 6 and 4. 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. + *

In our example and with a passed search range of 7 - 2, the returned keys would be 6 and 4. + * 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. + * Iterate backward between the passed keys. Do not return the "start" key, but do return the + * "stop" key. * - *

- * The "start" and "stop" values are both required. + *

The "start" and "stop" values are both required. * - *

- * In our example and with a passed search range of 7 - 2, the returned keys - * would be 6, 4 and 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. + *

In our example and with a passed search range of 7 - 2, the returned keys would be 6, 4 and + * 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) { + KeyRangeType( + final boolean directionForward, + final boolean startKeyRequired, + final boolean startKeyInclusive, + final boolean stopKeyRequired, + final boolean stopKeyInclusive) { this.directionForward = directionForward; this.startKeyRequired = startKeyRequired; + this.startKeyInclusive = startKeyInclusive; this.stopKeyRequired = stopKeyRequired; + this.stopKeyInclusive = stopKeyInclusive; } /** @@ -303,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. * @@ -312,12 +279,21 @@ 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. * - *

- * The iterator will perform this action and present the resulting key to - * {@link #iteratorOp(java.util.Comparator, java.lang.Object)} for decision. + *

The iterator will perform this action and present the resulting key to {@link + * #iteratorOp(java.util.Comparator, java.lang.Object)} for decision. * * @return appropriate action in response to this buffer */ @@ -367,17 +343,15 @@ CursorOp initialOp() { /** * Determine the iterator's response to the presented key. * - * @param buffer type - * @param comparator for the buffers - * @param start start buffer - * @param stop stop buffer + * @param buffer type + * @param comparator for the buffers * @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"); + > IteratorOp iteratorOp( + final T buffer, final RangeComparator rangeComparator) { + requireNonNull(rangeComparator, "Comparator required"); if (buffer == null) { return TERMINATE; } @@ -387,67 +361,66 @@ > IteratorOp iteratorOp(final T start, final T stop, 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"); } } /** - * Determine the iterator action to take when "next" is called or upon request - * of {@link #iteratorOp(java.util.Comparator, java.lang.Object)}. + * Determine the iterator action to take when "next" is called or upon request of {@link + * #iteratorOp(java.util.Comparator, java.lang.Object)}. * - *

- * The iterator will perform this action and present the resulting key to - * {@link #iteratorOp(java.util.Comparator, java.lang.Object)} for decision. + *

The iterator will perform this action and present the resulting key to {@link + * #iteratorOp(java.util.Comparator, java.lang.Object)} for decision. * * @return appropriate action for this key range type */ @@ -455,51 +428,29 @@ CursorOp nextOp() { return isDirectionForward() ? NEXT : PREV; } - /** - * Action now required with the iterator. - */ + /** Action now required with the iterator. */ enum IteratorOp { - /** - * Consider iterator completed. - */ + /** Consider iterator completed. */ TERMINATE, - /** - * Call {@link KeyRange#nextOp()} again and try again. - */ + /** Call {@link KeyRange#nextOp()} again and try again. */ CALL_NEXT_OP, - /** - * Return the key to the user. - */ + /** Return the key to the user. */ RELEASE } - /** - * Action now required with the cursor. - */ + /** Action now required with the cursor. */ enum CursorOp { - /** - * Move to first. - */ + /** Move to first. */ FIRST, - /** - * Move to last. - */ + /** Move to last. */ LAST, - /** - * Get "start" key with {@link GetOp#MDB_SET_RANGE}. - */ + /** Get "start" key with {@link GetOp#MDB_SET_RANGE}. */ GET_START_KEY, - /** - * Get "start" key with {@link GetOp#MDB_SET_RANGE}, fall back to LAST. - */ + /** Get "start" key with {@link GetOp#MDB_SET_RANGE}, fall back to LAST. */ GET_START_KEY_BACKWARD, - /** - * Move forward. - */ + /** Move forward. */ NEXT, - /** - * Move backward. - */ + /** Move backward. */ PREV } } diff --git a/src/main/java/org/lmdbjava/KeyVal.java b/src/main/java/org/lmdbjava/KeyVal.java index 12b9f9af..e2d2529f 100644 --- a/src/main/java/org/lmdbjava/KeyVal.java +++ b/src/main/java/org/lmdbjava/KeyVal.java @@ -1,23 +1,18 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static java.util.Objects.requireNonNull; @@ -41,9 +36,7 @@ final class KeyVal implements AutoCloseable { private final BufferProxy proxy; private final Pointer ptrArray; private final Pointer ptrKey; - private final long ptrKeyAddr; private final Pointer ptrVal; - private final long ptrValAddr; private T v; KeyVal(final BufferProxy proxy) { @@ -52,10 +45,8 @@ final class KeyVal implements AutoCloseable { this.k = proxy.allocate(); this.v = proxy.allocate(); ptrKey = MEM_MGR.allocateTemporary(MDB_VAL_STRUCT_SIZE, false); - ptrKeyAddr = ptrKey.address(); ptrArray = MEM_MGR.allocateTemporary(MDB_VAL_STRUCT_SIZE * 2, false); ptrVal = ptrArray.slice(0, MDB_VAL_STRUCT_SIZE); - ptrValAddr = ptrVal.address(); } @Override @@ -72,12 +63,12 @@ T key() { return k; } - void keyIn(final T key) { - proxy.in(key, ptrKey, ptrKeyAddr); + Pointer keyIn(final T key) { + return proxy.in(key, ptrKey); } T keyOut() { - k = proxy.out(k, ptrKey, ptrKeyAddr); + k = proxy.out(k, ptrKey); return k; } @@ -93,36 +84,35 @@ T val() { return v; } - void valIn(final T val) { - proxy.in(val, ptrVal, ptrValAddr); + Pointer valIn(final T val) { + return proxy.in(val, ptrVal); } - void valIn(final int size) { - proxy.in(v, size, ptrVal, ptrValAddr); + Pointer valIn(final int size) { + return proxy.in(v, size, ptrVal); } /** - * Prepares an array suitable for presentation as the data argument to a - * MDB_MULTIPLE put. + * Prepares an array suitable for presentation as the data argument to a MDB_MULTIPLE + * put. * - *

- * The returned array is equivalent of two MDB_vals as follows: + *

The returned array is equivalent of two MDB_vals as follows: * *

    - *
  • ptrVal1.data = pointer to the data address of passed buffer
  • - *
  • ptrVal1.size = size of each individual data element
  • - *
  • ptrVal2.data = unused
  • - *
  • ptrVal2.size = number of data elements (as passed to this method)
  • + *
  • ptrVal1.data = pointer to the data address of passed buffer + *
  • ptrVal1.size = size of each individual data element + *
  • ptrVal2.data = unused + *
  • ptrVal2.size = number of data elements (as passed to this method) *
* - * @param val a user-provided buffer with data elements (required) + * @param val a user-provided buffer with data elements (required) * @param elements number of data elements the user has provided * @return a properly-prepared pointer to an array for the operation */ Pointer valInMulti(final T val, final int elements) { final long ptrVal2SizeOff = MDB_VAL_STRUCT_SIZE + STRUCT_FIELD_OFFSET_SIZE; ptrArray.putLong(ptrVal2SizeOff, elements); // ptrVal2.size - proxy.in(val, ptrVal, ptrValAddr); // ptrVal1.data + proxy.in(val, ptrVal); // ptrVal1.data final long totalBufferSize = ptrVal.getLong(STRUCT_FIELD_OFFSET_SIZE); final long elemSize = totalBufferSize / elements; ptrVal.putLong(STRUCT_FIELD_OFFSET_SIZE, elemSize); // ptrVal1.size @@ -131,8 +121,7 @@ Pointer valInMulti(final T val, final int elements) { } T valOut() { - v = proxy.out(v, ptrVal, ptrValAddr); + v = proxy.out(v, ptrVal); return v; } - } diff --git a/src/main/java/org/lmdbjava/Library.java b/src/main/java/org/lmdbjava/Library.java index 22308600..6d8122d2 100644 --- a/src/main/java/org/lmdbjava/Library.java +++ b/src/main/java/org/lmdbjava/Library.java @@ -1,23 +1,18 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static java.io.File.createTempFile; @@ -32,8 +27,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import jnr.ffi.Pointer; import jnr.ffi.Struct; import jnr.ffi.annotations.Delegate; @@ -47,25 +40,23 @@ /** * JNR-FFI interface to LMDB. * - *

- * For performance reasons pointers are used rather than structs. + *

For performance reasons pointers are used rather than structs. */ final class Library { /** - * Java system property name that can be set to the path of an existing - * directory into which the LMDB system library will be extracted from the - * LmdbJava JAR. If unspecified the LMDB system library is extracted to the - * java.io.tmpdir. Ignored if the LMDB system library is not - * being extracted from the LmdbJava JAR (as would be the case if other - * system properties defined in TargetName have been set). + * Java system property name that can be set to the path of an existing directory into which the + * LMDB system library will be extracted from the LmdbJava JAR. If unspecified the LMDB system + * library is extracted to the java.io.tmpdir. Ignored if the LMDB system library is + * not being extracted from the LmdbJava JAR (as would be the case if other system properties + * defined in TargetName have been set). */ public static final String LMDB_EXTRACT_DIR_PROP = "lmdbjava.extract.dir"; - /** - * Indicates the directory where the LMDB system library will be extracted. - */ - static final String EXTRACT_DIR = getProperty(LMDB_EXTRACT_DIR_PROP, - getProperty("java.io.tmpdir")); + + /** Indicates the directory where the LMDB system library will be extracted. */ + static final String EXTRACT_DIR = + getProperty(LMDB_EXTRACT_DIR_PROP, getProperty("java.io.tmpdir")); + static final Lmdb LIB; static final jnr.ffi.Runtime RUNTIME; @@ -82,10 +73,8 @@ final class Library { RUNTIME = getRuntime(LIB); } - private Library() { - } + private Library() {} - @SuppressFBWarnings("OBL_UNSATISFIED_OBLIGATION") // Spotbugs issue #432 private static String extract(final String name) { final String suffix = name.substring(name.lastIndexOf('.')); final File file; @@ -98,7 +87,7 @@ private static String extract(final String name) { file.deleteOnExit(); final ClassLoader cl = currentThread().getContextClassLoader(); try (InputStream in = cl.getResourceAsStream(name); - OutputStream out = Files.newOutputStream(file.toPath())) { + OutputStream out = Files.newOutputStream(file.toPath())) { requireNonNull(in, "Classpath resource not found"); int bytes; final byte[] buffer = new byte[4_096]; @@ -112,11 +101,7 @@ private static String extract(final String name) { } } - /** - * Structure to wrap a native MDB_envinfo. Not for external use. - */ - @SuppressWarnings({"checkstyle:TypeName", "checkstyle:VisibilityModifier", - "checkstyle:MemberName"}) + /** Structure to wrap a native MDB_envinfo. Not for external use. */ public static final class MDB_envinfo extends Struct { public final Pointer f0_me_mapaddr; @@ -137,11 +122,7 @@ public static final class MDB_envinfo extends Struct { } } - /** - * Structure to wrap a native MDB_stat. Not for external use. - */ - @SuppressWarnings({"checkstyle:TypeName", "checkstyle:VisibilityModifier", - "checkstyle:MemberName"}) + /** Structure to wrap a native MDB_stat. Not for external use. */ public static final class MDB_stat extends Struct { public final u_int32_t f0_ms_psize; @@ -162,20 +143,14 @@ public static final class MDB_stat extends Struct { } } - /** - * Custom comparator callback used by mdb_set_compare. - */ + /** Custom comparator callback used by mdb_set_compare. */ public interface ComparatorCallback { @Delegate int compare(@In Pointer keyA, @In Pointer keyB); - } - /** - * JNR API for MDB-defined C functions. Not for external use. - */ - @SuppressWarnings("checkstyle:MethodName") + /** JNR API for MDB-defined C functions. Not for external use. */ public interface Lmdb { void mdb_cursor_close(@In Pointer cursor); @@ -184,27 +159,21 @@ public interface Lmdb { int mdb_cursor_del(@In Pointer cursor, int flags); - int mdb_cursor_get(@In Pointer cursor, Pointer k, @Out Pointer v, - int cursorOp); + int mdb_cursor_get(@In Pointer cursor, Pointer k, @Out Pointer v, int cursorOp); - int mdb_cursor_open(@In Pointer txn, @In Pointer dbi, - PointerByReference cursorPtr); + int mdb_cursor_open(@In Pointer txn, @In Pointer dbi, PointerByReference cursorPtr); - int mdb_cursor_put(@In Pointer cursor, @In Pointer key, @In Pointer data, - int flags); + int mdb_cursor_put(@In Pointer cursor, @In Pointer key, @In Pointer data, int flags); int mdb_cursor_renew(@In Pointer txn, @In Pointer cursor); void mdb_dbi_close(@In Pointer env, @In Pointer dbi); - int mdb_dbi_flags(@In Pointer txn, @In Pointer dbi, - @Out IntByReference flags); + int mdb_dbi_flags(@In Pointer txn, @In Pointer dbi, @Out IntByReference flags); - int mdb_dbi_open(@In Pointer txn, @In byte[] name, int flags, - @In Pointer dbiPtr); + int mdb_dbi_open(@In Pointer txn, @In byte[] name, int flags, @In Pointer dbiPtr); - int mdb_del(@In Pointer txn, @In Pointer dbi, @In Pointer key, - @In Pointer data); + int mdb_del(@In Pointer txn, @In Pointer dbi, @In Pointer key, @In Pointer data); int mdb_drop(@In Pointer txn, @In Pointer dbi, int del); @@ -240,12 +209,9 @@ int mdb_del(@In Pointer txn, @In Pointer dbi, @In Pointer key, int mdb_env_sync(@In Pointer env, int f); - int mdb_get(@In Pointer txn, @In Pointer dbi, @In Pointer key, - @Out Pointer data); + int mdb_get(@In Pointer txn, @In Pointer dbi, @In Pointer key, @Out Pointer data); - int mdb_put(@In Pointer txn, @In Pointer dbi, @In Pointer key, - @In Pointer data, - int flags); + int mdb_put(@In Pointer txn, @In Pointer dbi, @In Pointer key, @In Pointer data, int flags); int mdb_reader_check(@In Pointer env, @Out IntByReference dead); @@ -257,8 +223,7 @@ int mdb_put(@In Pointer txn, @In Pointer dbi, @In Pointer key, void mdb_txn_abort(@In Pointer txn); - int mdb_txn_begin(@In Pointer env, @In Pointer parentTx, int flags, - Pointer txPtr); + int mdb_txn_begin(@In Pointer env, @In Pointer parentTx, int flags, Pointer txPtr); int mdb_txn_commit(@In Pointer txn); @@ -270,8 +235,8 @@ int mdb_txn_begin(@In Pointer env, @In Pointer parentTx, int flags, void mdb_txn_reset(@In Pointer txn); - Pointer mdb_version(IntByReference major, IntByReference minor, - IntByReference patch); + 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/LmdbException.java b/src/main/java/org/lmdbjava/LmdbException.java index b3aea56e..c2624f95 100644 --- a/src/main/java/org/lmdbjava/LmdbException.java +++ b/src/main/java/org/lmdbjava/LmdbException.java @@ -1,28 +1,21 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; -/** - * Superclass for all LmdbJava custom exceptions. - */ +/** Superclass for all LmdbJava custom exceptions. */ public class LmdbException extends RuntimeException { private static final long serialVersionUID = 1L; @@ -40,10 +33,9 @@ public LmdbException(final String message) { * Constructs an instance with the provided detailed message and cause. * * @param message the detail message - * @param cause original cause + * @param cause original cause */ public LmdbException(final String message, final Throwable cause) { super(message, cause); } - } diff --git a/src/main/java/org/lmdbjava/LmdbNativeException.java b/src/main/java/org/lmdbjava/LmdbNativeException.java index 3f559806..5c094e77 100644 --- a/src/main/java/org/lmdbjava/LmdbNativeException.java +++ b/src/main/java/org/lmdbjava/LmdbNativeException.java @@ -1,44 +1,35 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static java.lang.String.format; -/** - * Superclass for all exceptions that originate from a native C call. - */ +/** Superclass for all exceptions that originate from a native C call. */ public class LmdbNativeException extends LmdbException { private static final long serialVersionUID = 1L; - /** - * Result code returned by the LMDB C function. - */ + /** Result code returned by the LMDB C function. */ private final int rc; /** * Constructs an instance with the provided detailed message. * * @param msg the detail message. - * @param rc the result code. + * @param rc the result code. */ LmdbNativeException(final int rc, final String msg) { super(format(msg + " (%d)", rc)); @@ -54,9 +45,7 @@ public final int getResultCode() { return rc; } - /** - * Exception raised from a system constant table lookup. - */ + /** Exception raised from a system constant table lookup. */ public static final class ConstantDerivedException extends LmdbNativeException { private static final long serialVersionUID = 1L; @@ -66,9 +55,7 @@ public static final class ConstantDerivedException extends LmdbNativeException { } } - /** - * Located page was wrong type. - */ + /** Located page was wrong type. */ public static final class PageCorruptedException extends LmdbNativeException { static final int MDB_CORRUPTED = -30_796; @@ -79,9 +66,7 @@ public static final class PageCorruptedException extends LmdbNativeException { } } - /** - * Page has not enough space - internal error. - */ + /** Page has not enough space - internal error. */ public static final class PageFullException extends LmdbNativeException { static final int MDB_PAGE_FULL = -30_786; @@ -92,37 +77,29 @@ public static final class PageFullException extends LmdbNativeException { } } - /** - * Requested page not found - this usually indicates corruption. - */ + /** Requested page not found - this usually indicates corruption. */ public static final class PageNotFoundException extends LmdbNativeException { static final int MDB_PAGE_NOTFOUND = -30_797; private static final long serialVersionUID = 1L; PageNotFoundException() { - super(MDB_PAGE_NOTFOUND, - "Requested page not found - this usually indicates corruption"); + super(MDB_PAGE_NOTFOUND, "Requested page not found - this usually indicates corruption"); } } - /** - * Update of meta page failed or environment had fatal error. - */ + /** Update of meta page failed or environment had fatal error. */ public static final class PanicException extends LmdbNativeException { static final int MDB_PANIC = -30_795; private static final long serialVersionUID = 1L; PanicException() { - super(MDB_PANIC, - "Update of meta page failed or environment had fatal error"); + super(MDB_PANIC, "Update of meta page failed or environment had fatal error"); } } - /** - * Too many TLS keys in use - Windows only. - */ + /** Too many TLS keys in use - Windows only. */ public static final class TlsFullException extends LmdbNativeException { static final int MDB_TLS_FULL = -30_789; diff --git a/src/main/java/org/lmdbjava/MaskedFlag.java b/src/main/java/org/lmdbjava/MaskedFlag.java index 9bdef636..67afebce 100644 --- a/src/main/java/org/lmdbjava/MaskedFlag.java +++ b/src/main/java/org/lmdbjava/MaskedFlag.java @@ -1,32 +1,30 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static java.util.Objects.requireNonNull; -/** - * Indicates an enum that can provide integers for each of its values. - */ +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. * @@ -34,33 +32,74 @@ public interface MaskedFlag { */ int getMask(); + /** + * The name of the flag. + * + * @return The name of the flag. + */ + String name(); + /** * Fetch the integer mask for all presented flags. * + * @param flag type * @param flags to mask (null or empty returns zero) * @return the integer mask for use in C */ - static int mask(final MaskedFlag... flags) { + @SafeVarargs + static int mask(final M... flags) { if (flags == null || flags.length == 0) { - return 0; + return EMPTY_MASK; + } else { + int result = EMPTY_MASK; + for (MaskedFlag flag : flags) { + if (flag == null) { + continue; + } + result |= flag.getMask(); + } + return result; } + } - int result = 0; - for (final MaskedFlag flag : flags) { - if (flag == null) { - continue; + /** + * Combine the two masks into a single mask value, i.e. when combining two {@link FlagSet}s. + * + * @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 int mask1, final int mask2) { + return mask1 | mask2; + } + + /** + * Fetch the integer mask for the presented flags. + * + * @param flag type + * @param flags to mask (null or empty returns zero) + * @return the integer mask for use in C + */ + 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(); } - result |= flag.getMask(); + return result; } - return result; } /** * Indicates whether the passed flag has the relevant masked flag high. * - * @param flags to evaluate (usually produced by - * {@link #mask(org.lmdbjava.MaskedFlag...)} - * @param test the flag being sought (required) + * @param flags to evaluate (usually produced by {@link #mask(org.lmdbjava.MaskedFlag...)} + * @param test the flag being sought (required) * @return true if set. */ static boolean isSet(final int flags, final MaskedFlag test) { diff --git a/src/main/java/org/lmdbjava/Meta.java b/src/main/java/org/lmdbjava/Meta.java index 6f392701..9f51e1af 100644 --- a/src/main/java/org/lmdbjava/Meta.java +++ b/src/main/java/org/lmdbjava/Meta.java @@ -1,46 +1,36 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static org.lmdbjava.Library.LIB; import jnr.ffi.byref.IntByReference; -/** - * LMDB metadata functions. - */ +/** LMDB metadata functions. */ public final class Meta { - private Meta() { - } + private Meta() {} /** * Fetches the LMDB error code description. * - *

- * End users should not need this method, as LmdbJava converts all LMDB - * exceptions into a typed Java exception that incorporates the error code. - * However it is provided here for verification and troubleshooting (eg if the - * user wishes to see the original LMDB description of the error code, or - * there is a newer library version etc). + *

End users should not need this method, as LmdbJava converts all LMDB exceptions into a typed + * Java exception that incorporates the error code. However it is provided here for verification + * and troubleshooting (eg if the user wishes to see the original LMDB description of the error + * code, or there is a newer library version etc). * * @param err the error code returned from LMDB * @return the description @@ -61,28 +51,19 @@ public static Version version() { LIB.mdb_version(major, minor, patch); - return new Version(major.intValue(), minor.intValue(), patch. - intValue()); + return new Version(major.intValue(), minor.intValue(), patch.intValue()); } - /** - * Immutable return value from {@link #version()}. - */ + /** Immutable return value from {@link #version()}. */ public static final class Version { - /** - * LMDC native library major version number. - */ + /** LMDC native library major version number. */ public final int major; - /** - * LMDC native library patch version number. - */ + /** LMDC native library patch version number. */ public final int minor; - /** - * LMDC native library patch version number. - */ + /** LMDC native library patch version number. */ public final int patch; Version(final int major, final int minor, final int patch) { @@ -91,5 +72,4 @@ public static final class Version { this.patch = patch; } } - } 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 48bc243b..2c400bae 100644 --- a/src/main/java/org/lmdbjava/PutFlags.java +++ b/src/main/java/org/lmdbjava/PutFlags.java @@ -1,33 +1,27 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; -/** - * Flags for use when performing a "put". - */ -public enum PutFlags implements MaskedFlag { +import java.util.EnumSet; +import java.util.Set; - /** - * For put: Don't write if the key already exists. - */ +/** Flags for use when performing a "put". */ +public enum PutFlags implements MaskedFlag, PutFlagSet { + + /** For put: Don't write if the key already exists. */ MDB_NOOVERWRITE(0x10), /** * Only for #MDB_DUPSORT
@@ -35,26 +29,17 @@ public enum PutFlags implements MaskedFlag { * For mdb_cursor_del: remove all duplicate data items. */ MDB_NODUPDATA(0x20), - /** - * For mdb_cursor_put: overwrite the current key/data pair. - */ + /** For mdb_cursor_put: overwrite the current key/data pair. */ MDB_CURRENT(0x40), /** - * For put: Just reserve space for data, don't copy it. Return a pointer to - * the reserved space. + * For put: Just reserve space for data, don't copy it. Return a pointer to the reserved space. */ MDB_RESERVE(0x1_0000), - /** - * Data is being appended, don't split full pages. - */ + /** Data is being appended, don't split full pages. */ MDB_APPEND(0x2_0000), - /** - * Duplicate data is being appended, don't split full pages. - */ + /** Duplicate data is being appended, don't split full pages. */ MDB_APPENDDUP(0x4_0000), - /** - * Store multiple data items in one call. Only for #MDB_DUPFIXED. - */ + /** Store multiple data items in one call. Only for #MDB_DUPFIXED. */ MDB_MULTIPLE(0x8_0000); private final int mask; @@ -68,4 +53,28 @@ 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/ReferenceUtil.java b/src/main/java/org/lmdbjava/ReferenceUtil.java index 36bf9247..67273d05 100644 --- a/src/main/java/org/lmdbjava/ReferenceUtil.java +++ b/src/main/java/org/lmdbjava/ReferenceUtil.java @@ -1,67 +1,50 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -/** - * Supports creating strong references in manner compatible with Java 8. - */ +/** Supports creating strong references in manner compatible with Java 8. */ public final class ReferenceUtil { - private ReferenceUtil() { - } + private ReferenceUtil() {} /** - * Ensures that the object referenced by the given reference remains - * strongly reachable, regardless of any prior actions of the program - * that might otherwise cause the object to become unreachable. Thus, the - * referenced object is not reclaimable by garbage collection at least until - * after the invocation of this method. + * Ensures that the object referenced by the given reference remains strongly reachable, + * regardless of any prior actions of the program that might otherwise cause the object to become + * unreachable. Thus, the referenced object is not reclaimable by garbage collection at least + * until after the invocation of this method. * - *

- * Recent versions of the JDK have a nasty habit of prematurely deciding - * objects are unreachable (eg - * StackOverflow question - * 26642153. + *

Recent versions of the JDK have a nasty habit of prematurely deciding objects are + * unreachable (eg StackOverflow question 26642153. * - *

- * java.lang.ref.Reference.reachabilityFence offers a solution to - * this problem, but it was only introduced in Java 9. LmdbJava presently - * supports Java 8 and therefore this method provides an alternative. + *

java.lang.ref.Reference.reachabilityFence offers a solution to this problem, + * but it was only introduced in Java 9. LmdbJava presently supports Java 8 and therefore this + * method provides an alternative. * - *

- * This method is always implemented as a synchronization on {@code ref}. - * It is the caller's responsibility to ensure that this synchronization - * will not cause deadlock. + *

This method works because HotSpot JIT-compilers prune dead locals based on method bytecode + * analysis rather than optimized IR. As Vladimir Ivanov explains: "any usage of a local extends + * its live range, even if that usage is eliminated in generated code". The method call at the + * bytecode level is sufficient to keep the object alive through safepoints, preventing premature + * garbage collection during native operations. * - * @param ref the reference (null is acceptable but has no effect) - * @see Netty PR 8410 + * @param ref the reference + * @see + * Vladimir Ivanov on reachabilityFence implementation */ - @SuppressFBWarnings({"ESync_EMPTY_SYNC", "UC_USELESS_VOID_METHOD"}) public static void reachabilityFence0(final Object ref) { - if (ref != null) { - synchronized (ref) { - // Empty synchronized is ok: https://stackoverflow.com/a/31933260/1151521 - } - } + // Empty method body is intentional - the method call itself at bytecode level + // extends the object's live range per HotSpot JIT behavior } } diff --git a/src/main/java/org/lmdbjava/ResultCodeMapper.java b/src/main/java/org/lmdbjava/ResultCodeMapper.java index 809628ac..1e4d841e 100644 --- a/src/main/java/org/lmdbjava/ResultCodeMapper.java +++ b/src/main/java/org/lmdbjava/ResultCodeMapper.java @@ -1,23 +1,18 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static jnr.constants.ConstantSet.getConstantSet; @@ -31,16 +26,12 @@ /** * Maps a LMDB C result code to the equivalent Java exception. * - *

- * The immutable nature of all LMDB exceptions means the mapper internally - * maintains a table of them. + *

The immutable nature of all LMDB exceptions means the mapper internally maintains a table of + * them. */ -@SuppressWarnings("PMD.CyclomaticComplexity") final class ResultCodeMapper { - /** - * Successful result. - */ + /** Successful result. */ static final int MDB_SUCCESS = 0; private static final ConstantSet CONSTANTS; @@ -50,8 +41,7 @@ final class ResultCodeMapper { CONSTANTS = getConstantSet(POSIX_ERR_NO); } - private ResultCodeMapper() { - } + private ResultCodeMapper() {} /** * Checks the result code and raises an exception is not {@link #MDB_SUCCESS}. @@ -113,5 +103,4 @@ static void checkRc(final int rc) { final String msg = constant.name() + " " + constant.toString(); throw new LmdbNativeException.ConstantDerivedException(rc, msg); } - } diff --git a/src/main/java/org/lmdbjava/SeekOp.java b/src/main/java/org/lmdbjava/SeekOp.java index ed5fb47c..162aef49 100644 --- a/src/main/java/org/lmdbjava/SeekOp.java +++ b/src/main/java/org/lmdbjava/SeekOp.java @@ -1,100 +1,62 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; /** * Flags for use when performing a {@link Cursor#seek(org.lmdbjava.SeekOp)}. * - *

- * Unlike most other LMDB enums, this enum is not bit masked. + *

Unlike most other LMDB enums, this enum is not bit masked. */ public enum SeekOp { - /** - * Position at first key/data item. - */ + /** Position at first key/data item. */ MDB_FIRST(0), - /** - * Position at first data item of current key. Only for - * {@link DbiFlags#MDB_DUPSORT}. - */ + /** Position at first data item of current key. Only for {@link DbiFlags#MDB_DUPSORT}. */ MDB_FIRST_DUP(1), - /** - * Position at key/data pair. Only for {@link DbiFlags#MDB_DUPSORT}. - */ + /** Position at key/data pair. Only for {@link DbiFlags#MDB_DUPSORT}. */ MDB_GET_BOTH(2), - /** - * position at key, nearest data. Only for {@link DbiFlags#MDB_DUPSORT}. - */ + /** position at key, nearest data. Only for {@link DbiFlags#MDB_DUPSORT}. */ MDB_GET_BOTH_RANGE(3), - /** - * Return key/data at current cursor position. - */ + /** Return key/data at current cursor position. */ MDB_GET_CURRENT(4), /** - * Return key and up to a page of duplicate data items from current cursor - * position. Move cursor to prepare for {@link #MDB_NEXT_MULTIPLE}. Only for - * {@link DbiFlags#MDB_DUPSORT}. + * Return key and up to a page of duplicate data items from current cursor position. Move cursor + * to prepare for {@link #MDB_NEXT_MULTIPLE}. Only for {@link DbiFlags#MDB_DUPSORT}. */ MDB_GET_MULTIPLE(5), - /** - * Position at last key/data item. - */ + /** Position at last key/data item. */ MDB_LAST(6), - /** - * Position at last data item of current key. Only for - * {@link DbiFlags#MDB_DUPSORT}. - */ + /** Position at last data item of current key. Only for {@link DbiFlags#MDB_DUPSORT}. */ MDB_LAST_DUP(7), - /** - * Position at next data item. - */ + /** Position at next data item. */ MDB_NEXT(8), - /** - * Position at next data item of current key. Only for - * {@link DbiFlags#MDB_DUPSORT}. - */ + /** Position at next data item of current key. Only for {@link DbiFlags#MDB_DUPSORT}. */ MDB_NEXT_DUP(9), /** - * Return key and up to a page of duplicate data items from next cursor - * position. Move cursor to prepare for {@link #MDB_NEXT_MULTIPLE}. Only for - * {@link DbiFlags#MDB_DUPSORT}. + * Return key and up to a page of duplicate data items from next cursor position. Move cursor to + * prepare for {@link #MDB_NEXT_MULTIPLE}. Only for {@link DbiFlags#MDB_DUPSORT}. */ MDB_NEXT_MULTIPLE(10), - /** - * Position at first data item of next key. - */ + /** Position at first data item of next key. */ MDB_NEXT_NODUP(11), - /** - * Position at previous data item. - */ + /** Position at previous data item. */ MDB_PREV(12), - /** - * Position at previous data item of current key. - * {@link DbiFlags#MDB_DUPSORT}. - */ + /** Position at previous data item of current key. {@link DbiFlags#MDB_DUPSORT}. */ MDB_PREV_DUP(13), - /** - * Position at last data item of previous key. - */ + /** Position at last data item of previous key. */ MDB_PREV_NODUP(14); private final int code; @@ -111,5 +73,4 @@ public enum SeekOp { public int getCode() { return code; } - } diff --git a/src/main/java/org/lmdbjava/Stat.java b/src/main/java/org/lmdbjava/Stat.java index 42b344fb..d4995324 100644 --- a/src/main/java/org/lmdbjava/Stat.java +++ b/src/main/java/org/lmdbjava/Stat.java @@ -1,64 +1,48 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; -/** - * Statistics, as returned by {@link Env#stat()} and - * {@link Dbi#stat(org.lmdbjava.Txn)}. - */ +/** Statistics, as returned by {@link Env#stat()} and {@link Dbi#stat(org.lmdbjava.Txn)}. */ public final class Stat { - /** - * Number of internal (non-leaf) pages. - */ + /** Number of internal (non-leaf) pages. */ public final long branchPages; - /** - * Depth (height) of the B-tree. - */ + /** Depth (height) of the B-tree. */ public final int depth; - /** - * Number of data items. - */ + /** Number of data items. */ public final long entries; - /** - * Number of leaf pages. - */ + /** Number of leaf pages. */ public final long leafPages; - /** - * Number of overflow pages. - */ + /** Number of overflow pages. */ public final long overflowPages; - /** - * Size of a database page. This is currently the same for all databases. - */ + /** Size of a database page. This is currently the same for all databases. */ public final int pageSize; - Stat(final int pageSize, final int depth, final long branchPages, - final long leafPages, - final long overflowPages, final long entries) { + Stat( + final int pageSize, + final int depth, + final long branchPages, + final long leafPages, + final long overflowPages, + final long entries) { this.pageSize = pageSize; this.depth = depth; this.branchPages = branchPages; @@ -69,10 +53,19 @@ public final class Stat { @Override public String toString() { - return "Stat{" + "branchPages=" + branchPages + ", depth=" + depth - + ", entries=" + entries + ", leafPages=" + leafPages - + ", overflowPages=" + overflowPages + ", pageSize=" + pageSize - + '}'; + return "Stat{" + + "branchPages=" + + branchPages + + ", depth=" + + depth + + ", entries=" + + entries + + ", leafPages=" + + leafPages + + ", overflowPages=" + + overflowPages + + ", pageSize=" + + pageSize + + '}'; } - } diff --git a/src/main/java/org/lmdbjava/TargetName.java b/src/main/java/org/lmdbjava/TargetName.java index 446ea5b5..49a65dea 100644 --- a/src/main/java/org/lmdbjava/TargetName.java +++ b/src/main/java/org/lmdbjava/TargetName.java @@ -1,69 +1,66 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static java.lang.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. * - *

- * Users will typically use an LMDB native library that is embedded within the - * LmdbJava JAR. Embedded libraries are built by a Zig cross-compilation step as - * part of the release process. The naming convention reflects the Zig target - * name plus a common filename extension. This simplifies support for future Zig - * targets (eg with different toolchains etc). + *

Users will typically use an LMDB native library that is embedded within the LmdbJava JAR. + * Embedded libraries are built by a Zig cross-compilation step as part of the release process. The + * naming convention reflects the Zig target name plus a common filename extension. This simplifies + * support for future Zig targets (eg with different toolchains etc). * - *

- * Users can set two system properties to override the automatic resolution of - * an embedded library. Setting {@link #LMDB_NATIVE_LIB_PROP} will force use of - * that external LMDB library. Setting {@link #LMDB_EMBEDDED_LIB_PROP} will - * force use of that embedded LMDB library. If both are set, the former property - * will take precedence. Most users do not need to set either property. + *

Users can set two system properties to override the automatic resolution of an embedded + * library. Setting {@link #LMDB_NATIVE_LIB_PROP} will force use of that external LMDB library. + * Setting {@link #LMDB_EMBEDDED_LIB_PROP} will force use of that embedded LMDB library. If both are + * set, the former property will take precedence. Most users do not need to set either property. */ public final class TargetName { /** - * True if the resolved native filename is an external file (conversely false - * indicates the file should be considered a classpath resource). + * True if the resolved native filename is an external file (conversely false indicates the file + * should be considered a classpath resource). */ public static final boolean IS_EXTERNAL; /** - * Java system property name that can be set to override the embedded library - * that will be used. This is likely to be required if automatic resolution - * fails but the user still prefers to use an LmdbJava-bundled library. This - * path must include the classpath prefix (usually org/lmdbjava). + * Java system property name that can be set to override the embedded library that will be used. + * This is likely to be required if automatic resolution fails but the user still prefers to use + * an LmdbJava-bundled library. This path must include the classpath prefix (usually + * org/lmdbjava/native). */ public static final String LMDB_EMBEDDED_LIB_PROP = "lmdbjava.embedded.lib"; + /** - * Java system property name that can be set to provide a custom path to an - * external LMDB system library. + * Java system property name that can be set to provide a custom path to an external LMDB system + * library. */ public static final String LMDB_NATIVE_LIB_PROP = "lmdbjava.native.lib"; - /** - * Resolved target native filename or fully-qualified classpath location. - */ + + /** Resolved target native filename or fully-qualified classpath location. */ public static final String RESOLVED_FILENAME; + private static final String ARCH = getProperty("os.arch"); private static final String EMBED = getProperty(LMDB_EMBEDDED_LIB_PROP); private static final String EXTERNAL = getProperty(LMDB_NATIVE_LIB_PROP); @@ -74,9 +71,14 @@ public final class TargetName { RESOLVED_FILENAME = resolveFilename(EXTERNAL, EMBED, ARCH, OS); } - private TargetName() { - } + private TargetName() {} + /** + * Resolves the filename extension of the bundled LMDB library for a given operating system. + * + * @param os typically the os.name system property + * @return extension of the LMDB system library bundled with LmdbJava + */ public static String resolveExtension(final String os) { return check(os, "Windows") ? "dll" : "so"; } @@ -85,8 +87,8 @@ static boolean isExternal(final String external) { return external != null && !external.isEmpty(); } - static String resolveFilename(final String external, final String embed, - final String arch, final String os) { + static String resolveFilename( + final String external, final String embed, final String arch, final String os) { if (external != null && !external.isEmpty()) { return external; } @@ -96,20 +98,25 @@ static String resolveFilename(final String external, final String embed, } final String pkg = TargetName.class.getPackage().getName().replace('.', '/'); - return pkg + "/" + resolveArch(arch) + "-" + resolveOs(os) + "-" - + resolveToolchain(os) + "." + resolveExtension(os); + return pkg + + "/native/" + + resolveArch(arch) + + "-" + + resolveOs(os) + + "-" + + resolveToolchain(os) + + "." + + resolveExtension(os); } /** - * Case insensitively checks whether the passed string starts with any of the - * candidate strings. + * Case insensitively checks whether the passed string starts with any of the candidate strings. * - * @param string the string being checked + * @param string the string being checked * @param candidates one or more candidate strings * @return true if the string starts with any of the candidates */ - private static boolean check(final String string, - final String... candidates) { + private static boolean check(final String string, final String... candidates) { if (string == null) { return false; } @@ -124,10 +131,19 @@ private static boolean check(final String string, } 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 " - + LMDB_EMBEDDED_LIB_PROP + " to the name of an LmdbJava embedded" - + " library; os.arch='" + ARCH + "' os.name='" + OS + "')"; + return reason + + " (please set system property " + + LMDB_NATIVE_LIB_PROP + + " 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='" + + ARCH + + "' os.name='" + + OS + + "')"; } private static String resolveArch(final String arch) { @@ -151,7 +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 b6d28917..7e9aacf9 100644 --- a/src/main/java/org/lmdbjava/Txn.java +++ b/src/main/java/org/lmdbjava/Txn.java @@ -1,23 +1,18 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static jnr.ffi.Memory.allocateDirect; @@ -25,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; @@ -34,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; /** @@ -51,12 +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(flags); - this.readOnly = isSet(flagsMask, MDB_RDONLY_TXN); + this.readOnly = flags.isSet(MDB_RDONLY_TXN); if (env.isReadOnly() && !this.readOnly) { throw new EnvIsReadOnly(); } @@ -67,15 +62,13 @@ 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; } - /** - * Aborts this transaction. - */ + /** Aborts this transaction. */ public void abort() { if (SHOULD_CHECK) { env.checkNotClosed(); @@ -88,10 +81,8 @@ public void abort() { /** * Closes this transaction by aborting if not already committed. * - *

- * Closing the transaction will invoke - * {@link BufferProxy#deallocate(java.lang.Object)} for each read-only buffer - * (ie the key and value). + *

Closing the transaction will invoke {@link BufferProxy#deallocate(java.lang.Object)} for + * each read-only buffer (ie the key and value). */ @Override public void close() { @@ -108,9 +99,7 @@ public void close() { state = RELEASED; } - /** - * Commits this transaction. - */ + /** Commits this transaction. */ public void commit() { if (SHOULD_CHECK) { env.checkNotClosed(); @@ -151,10 +140,10 @@ public boolean isReadOnly() { } /** - * Fetch the buffer which holds a read-only view of the LMDI allocated memory. - * Any use of this buffer must comply with the standard LMDB C "mdb_get" - * contract (ie do not modify, do not attempt to release the memory, do not - * use once the transaction or cursor closes, do not use after a write etc). + * Fetch the buffer which holds a read-only view of the LMDI allocated memory. Any use of this + * buffer must comply with the standard LMDB C "mdb_get" contract (ie do not modify, do not + * attempt to release the memory, do not use once the transaction or cursor closes, do not use + * after a write etc). * * @return the key buffer (never null) */ @@ -162,9 +151,7 @@ public T key() { return keyVal.key(); } - /** - * Renews a read-only transaction previously released by {@link #reset()}. - */ + /** Renews a read-only transaction previously released by {@link #reset()}. */ public void renew() { if (SHOULD_CHECK) { env.checkNotClosed(); @@ -178,8 +165,8 @@ public void renew() { } /** - * Aborts this read-only transaction and resets the transaction handle so it - * can be reused upon calling {@link #renew()}. + * Aborts this read-only transaction and resets the transaction handle, so it can be reused upon + * calling {@link #renew()}. */ public void reset() { if (SHOULD_CHECK) { @@ -194,10 +181,10 @@ public void reset() { } /** - * Fetch the buffer which holds a read-only view of the LMDI allocated memory. - * Any use of this buffer must comply with the standard LMDB C "mdb_get" - * contract (ie do not modify, do not attempt to release the memory, do not - * use once the transaction or cursor closes, do not use after a write etc). + * Fetch the buffer which holds a read-only view of the LMDI allocated memory. Any use of this + * buffer must comply with the standard LMDB C "mdb_get" contract (ie do not modify, do not + * attempt to release the memory, do not use once the transaction or cursor closes, do not use + * after a write etc). * * @return the value buffer (never null) */ @@ -244,9 +231,7 @@ Pointer pointer() { return ptr; } - /** - * Transaction must abort, has a child, or is invalid. - */ + /** Transaction must abort, has a child, or is invalid. */ public static final class BadException extends LmdbNativeException { static final int MDB_BAD_TXN = -30_782; @@ -257,9 +242,7 @@ public static final class BadException extends LmdbNativeException { } } - /** - * Invalid reuse of reader locktable slot. - */ + /** Invalid reuse of reader locktable slot. */ public static final class BadReaderLockException extends LmdbNativeException { static final int MDB_BAD_RSLOT = -30_783; @@ -270,114 +253,84 @@ public static final class BadReaderLockException extends LmdbNativeException { } } - /** - * The proposed R-W transaction is incompatible with a R-O Env. - */ + /** The proposed R-W transaction is incompatible with a R-O Env. */ public static class EnvIsReadOnly extends LmdbException { private static final long serialVersionUID = 1L; - /** - * Creates a new instance. - */ + /** Creates a new instance. */ public EnvIsReadOnly() { super("Read-write Txn incompatible with read-only Env"); } } - /** - * The proposed transaction is incompatible with its parent transaction. - */ + /** The proposed transaction is incompatible with its parent transaction. */ public static class IncompatibleParent extends LmdbException { private static final long serialVersionUID = 1L; - /** - * Creates a new instance. - */ + /** Creates a new instance. */ public IncompatibleParent() { super("Transaction incompatible with its parent transaction"); } } - /** - * Transaction is not in a READY state. - */ + /** Transaction is not in a READY state. */ public static final class NotReadyException extends LmdbException { private static final long serialVersionUID = 1L; - /** - * Creates a new instance. - */ + /** Creates a new instance. */ public NotReadyException() { super("Transaction is not in ready state"); } } - /** - * The current transaction has not been reset. - */ + /** The current transaction has not been reset. */ public static class NotResetException extends LmdbException { private static final long serialVersionUID = 1L; - /** - * Creates a new instance. - */ + /** Creates a new instance. */ public NotResetException() { super("Transaction has not been reset"); } } - /** - * The current transaction is not a read-only transaction. - */ + /** The current transaction is not a read-only transaction. */ public static class ReadOnlyRequiredException extends LmdbException { private static final long serialVersionUID = 1L; - /** - * Creates a new instance. - */ + /** Creates a new instance. */ public ReadOnlyRequiredException() { super("Not a read-only transaction"); } } - /** - * The current transaction is not a read-write transaction. - */ + /** The current transaction is not a read-write transaction. */ public static class ReadWriteRequiredException extends LmdbException { private static final long serialVersionUID = 1L; - /** - * Creates a new instance. - */ + /** Creates a new instance. */ public ReadWriteRequiredException() { super("Not a read-write transaction"); } } - /** - * The current transaction has already been reset. - */ + /** The current transaction has already been reset. */ public static class ResetException extends LmdbException { private static final long serialVersionUID = 1L; - /** - * Creates a new instance. - */ + /** Creates a new instance. */ public ResetException() { super("Transaction has already been reset"); } } - /** - * Transaction has too many dirty pages. - */ + /** Transaction has too many dirty pages. */ public static final class TxFullException extends LmdbNativeException { static final int MDB_TXN_FULL = -30_788; @@ -388,11 +341,11 @@ public static final class TxFullException extends LmdbNativeException { } } - /** - * Transaction states. - */ + /** Transaction states. */ enum State { - READY, DONE, RESET, RELEASED + READY, + DONE, + RESET, + RELEASED } - } 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 0379bfa7..866e1f65 100644 --- a/src/main/java/org/lmdbjava/TxnFlags.java +++ b/src/main/java/org/lmdbjava/TxnFlags.java @@ -1,32 +1,27 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; -/** - * Flags for use when creating a {@link Txn}. - */ -public enum TxnFlags implements MaskedFlag { - /** - * Read only. - */ +import java.util.EnumSet; +import java.util.Set; + +/** Flags for use when creating a {@link Txn}. */ +public enum TxnFlags implements MaskedFlag, TxnFlagSet { + + /** Read only. */ MDB_RDONLY_TXN(0x2_0000); private final int mask; @@ -40,4 +35,28 @@ 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/UnsafeAccess.java b/src/main/java/org/lmdbjava/UnsafeAccess.java index 836cda75..5b3da017 100644 --- a/src/main/java/org/lmdbjava/UnsafeAccess.java +++ b/src/main/java/org/lmdbjava/UnsafeAccess.java @@ -1,56 +1,42 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static java.lang.Boolean.getBoolean; import java.lang.reflect.Field; - import sun.misc.Unsafe; -/** - * Provides access to Unsafe. - */ +/** Provides access to Unsafe. */ final class UnsafeAccess { - /** - * Java system property name that can be set to disable unsafe. - */ + /** Java system property name that can be set to disable unsafe. */ public static final String DISABLE_UNSAFE_PROP = "lmdbjava.disable.unsafe"; - /** - * Indicates whether unsafe use is allowed. - */ + /** Indicates whether unsafe use is allowed. */ public static final boolean ALLOW_UNSAFE = !getBoolean(DISABLE_UNSAFE_PROP); /** - * The actual unsafe. Guaranteed to be non-null if this class can access - * unsafe and {@link #ALLOW_UNSAFE} is true. In other words, this entire class - * will fail to initialize if unsafe is unavailable. This avoids callers from - * needing to deal with null checks. + * The actual unsafe. Guaranteed to be non-null if this class can access unsafe and {@link + * #ALLOW_UNSAFE} is true. In other words, this entire class will fail to initialize if unsafe is + * unavailable. This avoids callers from needing to deal with null checks. */ static final Unsafe UNSAFE; - /** - * Unsafe field name (used to reflectively obtain the unsafe instance). - */ + + /** Unsafe field name (used to reflectively obtain the unsafe instance). */ private static final String FIELD_NAME_THE_UNSAFE = "theUnsafe"; static { @@ -61,13 +47,13 @@ final class UnsafeAccess { final Field field = Unsafe.class.getDeclaredField(FIELD_NAME_THE_UNSAFE); field.setAccessible(true); UNSAFE = (Unsafe) field.get(null); - } catch (final NoSuchFieldException | SecurityException - | IllegalArgumentException | IllegalAccessException e) { + } catch (final NoSuchFieldException + | SecurityException + | IllegalArgumentException + | IllegalAccessException e) { throw new LmdbException("Unsafe unavailable", e); } } - private UnsafeAccess() { - } - + private UnsafeAccess() {} } diff --git a/src/main/java/org/lmdbjava/Verifier.java b/src/main/java/org/lmdbjava/Verifier.java index 9807d0b8..87351fb0 100644 --- a/src/main/java/org/lmdbjava/Verifier.java +++ b/src/main/java/org/lmdbjava/Verifier.java @@ -1,23 +1,18 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static java.nio.ByteOrder.BIG_ENDIAN; @@ -37,53 +32,42 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.zip.CRC32; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - /** * Verifies correct operation of LmdbJava in a given environment. * - *

- * Due to the large variety of operating systems and Java platforms typically - * used with LmdbJava, this class provides a convenient verification of correct - * operating behavior through a potentially long duration set of tests that - * carefully verify correct storage and retrieval of successively larger - * database entries. + *

Due to the large variety of operating systems and Java platforms typically used with LmdbJava, + * this class provides a convenient verification of correct operating behavior through a potentially + * long duration set of tests that carefully verify correct storage and retrieval of successively + * larger database entries. * - *

- * The verifier currently operates by incrementing a long - * identifier that deterministically maps to a given {@link Dbi} and value size. - * The key is simply the long identifier. The value commences with - * a CRC that includes the identifier and the random bytes of the value. Each - * entry is written out, and then the prior entry is retrieved using its key. - * The prior entry's value is evaluated for accuracy and then deleted. - * Transactions are committed in batches to ensure successive transactions - * correctly retrieve the results of earlier transactions. + *

The verifier currently operates by incrementing a long identifier that + * deterministically maps to a given {@link Dbi} and value size. The key is simply the long + * identifier. The value commences with a CRC that includes the identifier and the random + * bytes of the value. Each entry is written out, and then the prior entry is retrieved using its + * key. The prior entry's value is evaluated for accuracy and then deleted. Transactions are + * committed in batches to ensure successive transactions correctly retrieve the results of earlier + * transactions. * - *

- * Please note the verification approach may be modified in the future. + *

Please note the verification approach may be modified in the future. * - *

- * If an exception is raised by this class, please: + *

If an exception is raised by this class, please: * *

    - *
  1. Ensure the {@link Env} passed at construction time complies with the - * requirements specified at {@link #Verifier(org.lmdbjava.Env)}
  2. - *
  3. Attempt to use a different file system to store the database (be - * especially careful to not use network file systems, remote file systems, - * read-only file systems etc)
  4. - *
  5. Record the full exception message and stack trace, then run the verifier - * again to see if it fails at the same or a different point
  6. - *
  7. Raise a ticket on the LmdbJava Issue Tracker that confirms the above - * details along with the failing operating system and Java version
  8. + *
  9. Ensure the {@link Env} passed at construction time complies with the requirements specified + * at {@link #Verifier(org.lmdbjava.Env)} + *
  10. Attempt to use a different file system to store the database (be especially careful to not + * use network file systems, remote file systems, read-only file systems etc) + *
  11. Record the full exception message and stack trace, then run the verifier again to see if it + * fails at the same or a different point + *
  12. Raise a ticket on the LmdbJava Issue Tracker that confirms the above details along with the + * failing operating system and Java version *
- * */ public final class Verifier implements Callable { - /** - * Number of DBIs the created environment should allow. - */ + /** Number of DBIs the created environment should allow. */ public static final int DBI_COUNT = 5; + private static final int BATCH_SIZE = 64; private static final int BUFFER_LEN = 1_024 * BATCH_SIZE; private static final int CRC_LENGTH = Long.BYTES; @@ -102,19 +86,15 @@ public final class Verifier implements Callable { /** * Create an instance of the verifier. * - *

- * The caller must provide an {@link Env} configured with a suitable local - * storage location, maximum DBIs equal to {@link #DBI_COUNT}, and a - * map size large enough to accommodate the intended verification duration. + *

The caller must provide an {@link Env} configured with a suitable local storage location, + * maximum DBIs equal to {@link #DBI_COUNT}, and a map size large enough to accommodate the + * intended verification duration. * - *

- * ALL EXISTING DATA IN THE DATABASE WILL BE DELETED. The caller must not - * interact with the Env in any way (eg querying, transactions - * etc) while the verifier is executing. + *

ALL EXISTING DATA IN THE DATABASE WILL BE DELETED. The caller must not interact with the + * Env in any way (eg querying, transactions etc) while the verifier is executing. * * @param env target that complies with the above requirements (required) */ - @SuppressFBWarnings("EI_EXPOSE_REP2") public Verifier(final Env env) { requireNonNull(env); this.env = env; @@ -127,10 +107,8 @@ public Verifier(final Env env) { /** * Run the verifier until {@link #stop()} is called or an exception occurs. * - *

- * Successful return of this method indicates no faults were detected. If any - * fault was detected the exception message will detail the exact point that - * the fault was encountered. + *

Successful return of this method indicates no faults were detected. If any fault was + * detected the exception message will detail the exact point that the fault was encountered. * * @return number of database rows successfully verified */ @@ -159,12 +137,11 @@ public Long call() { /** * Execute the verifier for the given duration. * - *

- * This provides a simple way to execute the verifier for those applications - * which do not wish to manage threads directly. + *

This provides a simple way to execute the verifier for those applications which do not wish + * to manage threads directly. * * @param duration amount of time to execute - * @param unit units used to express the duration + * @param unit units used to express the duration * @return number of database rows successfully verified */ public long runFor(final long duration, final TimeUnit unit) { @@ -198,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(); @@ -233,14 +211,11 @@ private Dbi getDbi(final long forId) { return dbis.get((int) (forId % dbis.size())); } - /** - * Request the verifier to stop execution. - */ + /** Request the verifier to stop execution. */ private void stop() { proceed.set(false); } - @SuppressFBWarnings("DMI_RANDOM_USED_ONLY_ONCE") private void transactionControl() { if (id % BATCH_SIZE == 0) { if (txn != null) { @@ -282,8 +257,8 @@ private void verifyValue(final long forId, final ByteBuffer bb) { final int rndSize = valueSize(forId); final int expected = rndSize + CRC_LENGTH; if (bb.limit() != expected) { - throw new IllegalStateException("Limit error id=" + forId + " exp=" - + expected + " limit=" + bb.limit()); + throw new IllegalStateException( + "Limit error id=" + forId + " exp=" + expected + " limit=" + bb.limit()); } final long crcRead = bb.getLong(); @@ -308,5 +283,4 @@ private void write(final long forId) { throw new IllegalStateException("DB put id=" + forId, ex); } } - } diff --git a/src/main/java/org/lmdbjava/package-info.java b/src/main/java/org/lmdbjava/package-info.java index 52c8551a..9589c470 100644 --- a/src/main/java/org/lmdbjava/package-info.java +++ b/src/main/java/org/lmdbjava/package-info.java @@ -1,63 +1,52 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - /** * Lightning Memory Database (LMDB) for Java (LmdbJava). * - *

- * LmdbJava is intended for extremely low latency use cases. Users are required - * to understand and comply with the LMDB C API contract (eg handle usage - * patterns, thread binding, process rules). + *

LmdbJava is intended for extremely low latency use cases. Users are required to understand and + * comply with the LMDB C API contract (eg handle usage patterns, thread binding, process rules). + * + *

Priorities: * - *

- * Priorities: *

    - *
  1. Minimize latency, particularly on any critical path (see below)
  2. - *
  3. Preserve the LMDB C API model as far as practical
  4. - *
  5. Apply Java idioms only when not in conflict with the above
  6. - *
  7. Fully encapsulate (hide) the native call library and patterns
  8. - *
  9. Don't require runtime dependencies beyond the native call library
  10. - *
  11. Support official JVMs running on typical 64-bit operating systems
  12. - *
  13. Prepare for Java 9 (eg Unsafe, native call technology roadmap etc)
  14. + *
  15. Minimize latency, particularly on any critical path (see below) + *
  16. Preserve the LMDB C API model as far as practical + *
  17. Apply Java idioms only when not in conflict with the above + *
  18. Fully encapsulate (hide) the native call library and patterns + *
  19. Don't require runtime dependencies beyond the native call library + *
  20. Support official JVMs running on typical 64-bit operating systems + *
  21. Prepare for Java 9 (eg Unsafe, native call technology roadmap etc) *
* - *

- * Critical paths of special latency focus: + *

Critical paths of special latency focus: + * *

    - *
  • Releasing and renewing a read-only transaction
  • - *
  • Any operation that uses a cursor
  • + *
  • Releasing and renewing a read-only transaction + *
  • Any operation that uses a cursor *
* - *

- * The classes in LmdbJava DO NOT provide any concurrency guarantees. Instead - * you MUST observe LMDB's specific thread rules (eg do not share transactions - * between threads). LmdbJava does not shield you from these requirements, as - * doing so would impose locking overhead on use cases that may not require it - * or have already carefully implemented application threading (as most low + *

The classes in LmdbJava DO NOT provide any concurrency guarantees. Instead you MUST observe + * LMDB's specific thread rules (eg do not share transactions between threads). LmdbJava does not + * shield you from these requirements, as doing so would impose locking overhead on use cases that + * may not require it or have already carefully implemented application threading (as most low * latency applications do to optimize the memory hierarchy, core pinning etc). * - *

- * Most methods in this package will throw a standard Java exception for failing - * preconditions (eg {@link NullPointerException} if a mandatory argument was - * missing) or a subclass of {@link LmdbException} for precondition or LMDB C - * failures. The majority of LMDB exceptions indicate an API usage or - * {@link Env} configuration issues, and as such are typically unrecoverable. + *

Most methods in this package will throw a standard Java exception for failing preconditions + * (eg {@link NullPointerException} if a mandatory argument was missing) or a subclass of {@link + * LmdbException} for precondition or LMDB C failures. The majority of LMDB exceptions indicate an + * API usage or {@link Env} configuration issues, and as such are typically unrecoverable. */ package org.lmdbjava; diff --git a/src/main/resources/org/lmdbjava/.gitignore b/src/main/resources/org/lmdbjava/.gitignore deleted file mode 100644 index 661f98b3..00000000 --- a/src/main/resources/org/lmdbjava/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.so -*.dll diff --git a/src/misc/license-template.txt b/src/misc/license-template.txt new file mode 100644 index 00000000..046ef77f --- /dev/null +++ b/src/misc/license-template.txt @@ -0,0 +1,13 @@ +Copyright © ${license.git.copyrightYears} ${owner} + +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. \ No newline at end of file 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 3daaea5f..2e4ed823 100644 --- a/src/test/java/org/lmdbjava/ByteBufferProxyTest.java +++ b/src/test/java/org/lmdbjava/ByteBufferProxyTest.java @@ -1,21 +1,17 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ package org.lmdbjava; @@ -25,11 +21,8 @@ import static java.nio.ByteBuffer.allocateDirect; import static java.nio.ByteOrder.BIG_ENDIAN; import static java.nio.ByteOrder.LITTLE_ENDIAN; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.hamcrest.MatcherAssert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.lmdbjava.BufferProxy.MDB_VAL_STRUCT_SIZE; import static org.lmdbjava.ByteBufferProxy.AbstractByteBufferProxy.findField; import static org.lmdbjava.ByteBufferProxy.PROXY_OPTIMAL; @@ -41,46 +34,55 @@ import static org.lmdbjava.TestUtils.invokePrivateConstructor; import static org.lmdbjava.UnsafeAccess.ALLOW_UNSAFE; -import java.io.File; -import java.io.IOException; 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.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; import org.lmdbjava.ByteBufferProxy.BufferMustBeDirectException; import org.lmdbjava.Env.ReadersFullException; -/** - * Test {@link ByteBufferProxy}. - */ +/** Test {@link ByteBufferProxy}. */ public final class ByteBufferProxyTest { static final MemoryManager MEM_MGR = RUNTIME.getMemoryManager(); - @Rule - public final TemporaryFolder tmp = new TemporaryFolder(); - - @Test(expected = BufferMustBeDirectException.class) - public void buffersMustBeDirect() throws IOException { - final File path = tmp.newFolder(); - try (Env env = create() - .setMaxReaders(1) - .open(path)) { - 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 - } + @Test + void buffersMustBeDirect() { + assertThatThrownBy( + () -> { + 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); } @Test - public void byteOrderResets() { + void byteOrderResets() { final int retries = 100; for (int i = 0; i < retries; i++) { final ByteBuffer bb = PROXY_OPTIMAL.allocate(); @@ -88,56 +90,222 @@ public void byteOrderResets() { PROXY_OPTIMAL.deallocate(bb); } for (int i = 0; i < retries; i++) { - assertThat(PROXY_OPTIMAL.allocate().order(), is(BIG_ENDIAN)); + assertThat(PROXY_OPTIMAL.allocate().order()).isEqualTo(BIG_ENDIAN); } } @Test - public void coverPrivateConstructor() { + void coverPrivateConstructor() { invokePrivateConstructor(ByteBufferProxy.class); } - @Test(expected = LmdbException.class) - public void fieldNeverFound() { - findField(Exception.class, "notARealField"); + @Test + void fieldNeverFound() { + assertThatThrownBy( + () -> { + findField(Exception.class, "notARealField"); + }) + .isInstanceOf(LmdbException.class); } @Test - public void fieldSuperclassScan() { + void fieldSuperclassScan() { final Field f = findField(ReadersFullException.class, "rc"); - assertThat(f, is(notNullValue())); + assertThat(f).isNotNull(); } @Test - public void inOutBuffersProxyOptimal() { + void inOutBuffersProxyOptimal() { checkInOut(PROXY_OPTIMAL); } @Test - public void inOutBuffersProxySafe() { + void inOutBuffersProxySafe() { checkInOut(PROXY_SAFE); } @Test - public void optimalAlwaysAvailable() { + void optimalAlwaysAvailable() { final BufferProxy v = PROXY_OPTIMAL; - assertThat(v, is(notNullValue())); + assertThat(v).isNotNull(); } @Test - public void safeCanBeForced() { + void safeCanBeForced() { final BufferProxy v = PROXY_SAFE; - assertThat(v, is(notNullValue())); - assertThat(v.getClass().getSimpleName(), startsWith("Reflect")); + assertThat(v).isNotNull(); + assertThat(v.getClass().getSimpleName()).startsWith("Reflect"); } @Test - public void unsafeIsDefault() { - assertThat(ALLOW_UNSAFE, is(true)); + void unsafeIsDefault() { + assertThat(ALLOW_UNSAFE).isTrue(); final BufferProxy v = PROXY_OPTIMAL; - assertThat(v, is(notNullValue())); - assertThat(v, is(not(PROXY_SAFE))); - assertThat(v.getClass().getSimpleName(), startsWith("Unsafe")); + assertThat(v).isNotNull(); + assertThat(v).isNotEqualTo(PROXY_SAFE); + 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) { @@ -150,14 +318,13 @@ private void checkInOut(final BufferProxy v) { b.position(BYTES); // skip 1 final Pointer p = MEM_MGR.allocateTemporary(MDB_VAL_STRUCT_SIZE, false); - v.in(b, p, p.address()); + v.in(b, p); final ByteBuffer bb = allocateDirect(1); - v.out(bb, p, p.address()); + v.out(bb, p); - assertThat(bb.getInt(), is(2)); - assertThat(bb.getInt(), is(3)); - assertThat(bb.remaining(), is(0)); + assertThat(bb.getInt()).isEqualTo(2); + assertThat(bb.getInt()).isEqualTo(3); + assertThat(bb.remaining()).isEqualTo(0); } - } 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 8371a1ae..5a1ddb62 100644 --- a/src/test/java/org/lmdbjava/ComparatorTest.java +++ b/src/test/java/org/lmdbjava/ComparatorTest.java @@ -1,57 +1,49 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ package org.lmdbjava; import static io.netty.buffer.PooledByteBufAllocator.DEFAULT; import static java.nio.charset.StandardCharsets.US_ASCII; -import static org.hamcrest.core.Is.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; 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.EQUAL_TO; -import static org.lmdbjava.ComparatorTest.ComparatorResult.GREATER_THAN; -import static org.lmdbjava.ComparatorTest.ComparatorResult.LESS_THAN; -import static org.lmdbjava.ComparatorTest.ComparatorResult.get; +import static org.lmdbjava.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 java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.Comparator; - import com.google.common.primitives.SignedBytes; import com.google.common.primitives.UnsignedBytes; import io.netty.buffer.ByteBuf; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Comparator; +import java.util.stream.Stream; import org.agrona.DirectBuffer; import org.agrona.concurrent.UnsafeBuffer; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameter; -import org.junit.runners.Parameterized.Parameters; - -/** - * Tests comparator functions are consistent across buffers. - */ -@RunWith(Parameterized.class) +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.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 { // H = 1 (high), L = 0 (low), X = byte not set in buffer @@ -66,22 +58,21 @@ public final class ComparatorTest { private static final byte[] LLLLLLLX = buffer(0, 0, 0, 0, 0, 0, 0); private static final byte[] LX = buffer(0); private static final byte[] XX = buffer(); - /** - * Injected by {@link #data()} with appropriate runner. - */ - @Parameter - public ComparatorRunner comparator; - - @Parameters(name = "{index}: comparable: {0}") - public static Object[] data() { - final ComparatorRunner string = new StringRunner(); - final ComparatorRunner db = new DirectBufferRunner(); - final ComparatorRunner ba = new ByteArrayRunner(); - final ComparatorRunner bb = new ByteBufferRunner(); - final ComparatorRunner netty = new NettyRunner(); - final ComparatorRunner gub = new GuavaUnsignedBytes(); - final ComparatorRunner gsb = new GuavaSignedBytes(); - return new Object[]{string, db, ba, bb, netty, gub, gsb}; + + 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) { @@ -92,57 +83,58 @@ private static byte[] buffer(final int... bytes) { return array; } - @Test - public void atLeastOneBufferHasEightBytes() { - assertThat(get(comparator.compare(HLLLLLLL, LLLLLLLL)), is(GREATER_THAN)); - assertThat(get(comparator.compare(LLLLLLLL, HLLLLLLL)), is(LESS_THAN)); + @ParameterizedTest + @ArgumentsSource(MyArgumentProvider.class) + void atLeastOneBufferHasEightBytes(final ComparatorRunner comparator) { + assertThat(TestUtils.compare(comparator, HLLLLLLL, LLLLLLLL)).isEqualTo(GREATER_THAN); + assertThat(TestUtils.compare(comparator, LLLLLLLL, HLLLLLLL)).isEqualTo(LESS_THAN); - assertThat(get(comparator.compare(LHLLLLLL, LLLLLLLL)), is(GREATER_THAN)); - assertThat(get(comparator.compare(LLLLLLLL, LHLLLLLL)), is(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)), is(GREATER_THAN)); - assertThat(get(comparator.compare(LLLLLLLX, LLLLLLLL)), is(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)), is(GREATER_THAN)); - assertThat(get(comparator.compare(HLLLLLLX, HLLLLLLL)), is(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)), is(GREATER_THAN)); - assertThat(get(comparator.compare(LHLLLLLL, HLLLLLLX)), is(LESS_THAN)); + assertThat(TestUtils.compare(comparator, HLLLLLLX, LHLLLLLL)).isEqualTo(GREATER_THAN); + assertThat(TestUtils.compare(comparator, LHLLLLLL, HLLLLLLX)).isEqualTo(LESS_THAN); } - @Test - public void buffersOfTwoBytes() { - assertThat(get(comparator.compare(LL, XX)), is(GREATER_THAN)); - assertThat(get(comparator.compare(XX, LL)), is(LESS_THAN)); + @ParameterizedTest + @ArgumentsSource(MyArgumentProvider.class) + void buffersOfTwoBytes(final ComparatorRunner comparator) { + assertThat(TestUtils.compare(comparator, LL, XX)).isEqualTo(GREATER_THAN); + assertThat(TestUtils.compare(comparator, XX, LL)).isEqualTo(LESS_THAN); - assertThat(get(comparator.compare(LL, LX)), is(GREATER_THAN)); - assertThat(get(comparator.compare(LX, LL)), is(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)), is(GREATER_THAN)); - assertThat(get(comparator.compare(LX, HL)), is(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)), is(GREATER_THAN)); - assertThat(get(comparator.compare(LH, HX)), is(LESS_THAN)); + assertThat(TestUtils.compare(comparator, HX, LL)).isEqualTo(GREATER_THAN); + assertThat(TestUtils.compare(comparator, LH, HX)).isEqualTo(LESS_THAN); } - @Test - public void equalBuffers() { - assertThat(get(comparator.compare(LL, LL)), is(EQUAL_TO)); - assertThat(get(comparator.compare(HX, HX)), is(EQUAL_TO)); - assertThat(get(comparator.compare(LH, LH)), is(EQUAL_TO)); - assertThat(get(comparator.compare(LL, LL)), is(EQUAL_TO)); - assertThat(get(comparator.compare(LX, LX)), is(EQUAL_TO)); - - assertThat(get(comparator.compare(HLLLLLLL, HLLLLLLL)), is(EQUAL_TO)); - assertThat(get(comparator.compare(HLLLLLLX, HLLLLLLX)), is(EQUAL_TO)); - assertThat(get(comparator.compare(LHLLLLLL, LHLLLLLL)), is(EQUAL_TO)); - assertThat(get(comparator.compare(LLLLLLLL, LLLLLLLL)), is(EQUAL_TO)); - assertThat(get(comparator.compare(LLLLLLLX, LLLLLLLX)), is(EQUAL_TO)); + @ParameterizedTest + @ArgumentsSource(MyArgumentProvider.class) + void equalBuffers(final ComparatorRunner comparator) { + 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}. - */ + /** Tests {@link ByteArrayProxy}. */ private static final class ByteArrayRunner implements ComparatorRunner { @Override @@ -152,9 +144,17 @@ public int compare(final byte[] o1, final byte[] o2) { } } - /** - * Tests {@link ByteBufferProxy}. - */ + /** Tests {@link ByteArrayProxy} (unsigned). */ + private static final class UnsignedByteArrayRunner implements ComparatorRunner { + + @Override + public int compare(final byte[] o1, final byte[] o2) { + final Comparator c = PROXY_BA.getComparator(); + return c.compare(o1, o2); + } + } + + /** Tests {@link ByteBufferProxy}. */ private static final class ByteBufferRunner implements ComparatorRunner { @Override @@ -172,14 +172,14 @@ public int compare(final byte[] o1, final byte[] o2) { o2b = arrayToBuffer(o2, o2.length * 3); final int result2 = c.compare(o1b, o2b); - assertThat(result2, is(result)); + assertThat(result2).isEqualTo(result); // Now try with buffers sized to the array. o1b = ByteBuffer.wrap(o1); o2b = ByteBuffer.wrap(o2); final int result3 = c.compare(o1b, o2b); - assertThat(result3, is(result)); + assertThat(result3).isEqualTo(result); return result; } @@ -196,9 +196,7 @@ private ByteBuffer arrayToBuffer(final byte[] arr, final int bufferCapacity) { } } - /** - * Tests {@link DirectBufferProxy}. - */ + /** Tests {@link DirectBufferProxy}. */ private static final class DirectBufferRunner implements ComparatorRunner { @Override @@ -210,9 +208,7 @@ public int compare(final byte[] o1, final byte[] o2) { } } - /** - * Tests using Guava's {@link SignedBytes} comparator. - */ + /** Tests using Guava's {@link SignedBytes} comparator. */ private static final class GuavaSignedBytes implements ComparatorRunner { @Override @@ -222,9 +218,7 @@ public int compare(final byte[] o1, final byte[] o2) { } } - /** - * Tests using Guava's {@link UnsignedBytes} comparator. - */ + /** Tests using Guava's {@link UnsignedBytes} comparator. */ private static final class GuavaUnsignedBytes implements ComparatorRunner { @Override @@ -234,9 +228,7 @@ public int compare(final byte[] o1, final byte[] o2) { } } - /** - * Tests {@link ByteBufProxy}. - */ + /** Tests {@link ByteBufProxy}. */ private static final class NettyRunner implements ComparatorRunner { @Override @@ -251,8 +243,8 @@ public int compare(final byte[] o1, final byte[] o2) { } /** - * Tests {@link String} by providing a reference implementation of what a - * comparator involving ASCII-encoded bytes should return. + * Tests {@link String} by providing a reference implementation of what a comparator involving + * ASCII-encoded bytes should return. */ private static final class StringRunner implements ComparatorRunner { @@ -264,36 +256,18 @@ 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 { + /** Interface that can test a {@link BufferProxy} compare method. */ + private interface ComparatorRunner extends Comparator { /** - * Convert the passed byte arrays into the proxy's relevant buffer type and - * then invoke the comparator. + * Convert the passed byte arrays into the proxy's relevant buffer type and then invoke the + * comparator. * * @param o1 lhs buffer content * @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 db28405a..d90c3c23 100644 --- a/src/test/java/org/lmdbjava/CursorIterableTest.java +++ b/src/test/java/org/lmdbjava/CursorIterableTest.java @@ -1,30 +1,24 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ package org.lmdbjava; -import static com.jakewharton.byteunits.BinaryByteUnit.KIBIBYTES; import static java.util.Arrays.asList; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasSize; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.lmdbjava.DbiFlags.MDB_CREATE; import static org.lmdbjava.Env.create; import static org.lmdbjava.EnvFlags.MDB_NOSUBDIR; @@ -48,11 +42,12 @@ 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 java.io.File; -import java.io.IOException; +import com.google.common.primitives.UnsignedBytes; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Comparator; @@ -61,236 +56,279 @@ import java.util.LinkedList; import java.util.List; import java.util.NoSuchElementException; - -import com.google.common.primitives.UnsignedBytes; -import org.hamcrest.Matchers; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; +import java.util.function.Function; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.Parameter; +import org.junit.jupiter.params.ParameterizedClass; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.support.ParameterDeclarations; import org.lmdbjava.CursorIterable.KeyVal; -/** - * Test {@link CursorIterable}. - */ +/** Test {@link CursorIterable}. */ +@ParameterizedClass(name = "{index}: dbi: {0}") +@ArgumentsSource(CursorIterableTest.MyArgumentProvider.class) public final class CursorIterableTest { - @Rule - public final TemporaryFolder tmp = new TemporaryFolder(); - 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; - @After - public void after() { + // /** + // * Injected by {@link #data()} with appropriate runner. + // */ + // @SuppressWarnings("ClassEscapesDefinedScope") + @Parameter public DbiFactory dbiFactory; + + @BeforeEach + void beforeEach() { + tempDir = new TempDir(); + final BufferProxy bufferProxy = ByteBufferProxy.PROXY_OPTIMAL; + env = + create(bufferProxy) + .setMapSize(256, ByteUnit.KIBIBYTES) + .setMaxReaders(1) + .setMaxDbs(3) + .setEnvFlags(MDB_NOSUBDIR) + .open(tempDir.createTempFile()); + + populateTestDataList(); + } + + @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); + c.put(bb(4), bb(5)); + c.put(bb(6), bb(7)); + c.put(bb(8), bb(9)); + txn.commit(); + } } @Test - public void allBackwardTest() { + void allBackwardTest() { verify(allBackward(), 8, 6, 4, 2); } @Test - public void allTest() { + void allTest() { verify(all(), 2, 4, 6, 8); } @Test - public void atLeastBackwardTest() { + void atLeastBackwardTest() { verify(atLeastBackward(bb(5)), 4, 2); verify(atLeastBackward(bb(6)), 6, 4, 2); verify(atLeastBackward(bb(9)), 8, 6, 4, 2); } @Test - public void atLeastTest() { + void atLeastTest() { verify(atLeast(bb(5)), 6, 8); verify(atLeast(bb(6)), 6, 8); } @Test - public void atMostBackwardTest() { + void atMostBackwardTest() { verify(atMostBackward(bb(5)), 8, 6); verify(atMostBackward(bb(6)), 8, 6); } @Test - public void atMostTest() { + void atMostTest() { verify(atMost(bb(5)), 2, 4); verify(atMost(bb(6)), 2, 4, 6); } - @Before - public void before() throws IOException { - final File path = tmp.newFile(); - env = create() - .setMapSize(KIBIBYTES.toBytes(256)) - .setMaxReaders(1) - .setMaxDbs(1) - .open(path, POSIX_MODE, MDB_NOSUBDIR); - db = env.openDbi(DB_1, MDB_CREATE); - populateDatabase(db); - } - - private void populateDatabase(final Dbi dbi) { - list = new LinkedList<>(); - list.addAll(asList(2, 3, 4, 5, 6, 7, 8, 9)); - try (Txn txn = env.txnWrite()) { - final Cursor c = dbi.openCursor(txn); - c.put(bb(2), bb(3), MDB_NOOVERWRITE); - c.put(bb(4), bb(5)); - c.put(bb(6), bb(7)); - c.put(bb(8), bb(9)); - txn.commit(); - } - } - @Test - public void closedBackwardTest() { + void closedBackwardTest() { verify(closedBackward(bb(7), bb(3)), 6, 4); verify(closedBackward(bb(6), bb(2)), 6, 4, 2); verify(closedBackward(bb(9), bb(3)), 8, 6, 4); } @Test - public void closedOpenBackwardTest() { + void closedOpenBackwardTest() { verify(closedOpenBackward(bb(8), bb(3)), 8, 6, 4); verify(closedOpenBackward(bb(7), bb(2)), 6, 4); verify(closedOpenBackward(bb(9), bb(3)), 8, 6, 4); } @Test - public void closedOpenTest() { + void closedOpenTest() { verify(closedOpen(bb(3), bb(8)), 4, 6); verify(closedOpen(bb(2), bb(6)), 2, 4); } @Test - public void closedTest() { + void closedTest() { verify(closed(bb(3), bb(7)), 4, 6); verify(closed(bb(2), bb(6)), 2, 4, 6); verify(closed(bb(1), bb(7)), 2, 4, 6); } @Test - public void greaterThanBackwardTest() { + void greaterThanBackwardTest() { verify(greaterThanBackward(bb(6)), 4, 2); verify(greaterThanBackward(bb(7)), 6, 4, 2); verify(greaterThanBackward(bb(9)), 8, 6, 4, 2); } @Test - public void greaterThanTest() { + void greaterThanTest() { verify(greaterThan(bb(4)), 6, 8); verify(greaterThan(bb(3)), 4, 6, 8); } - @Test(expected = IllegalStateException.class) - public void iterableOnlyReturnedOnce() { - try (Txn txn = env.txnRead(); - CursorIterable c = db.iterate(txn)) { - c.iterator(); // ok - c.iterator(); // fails - } + @Test + void iterableOnlyReturnedOnce() { + 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() { + void iterate() { + final Dbi db = getDb(); try (Txn txn = env.txnRead(); - CursorIterable c = db.iterate(txn)) { + CursorIterable c = db.iterate(txn)) { + for (final KeyVal kv : c) { - assertThat(kv.key().getInt(), is(list.pollFirst())); - assertThat(kv.val().getInt(), is(list.pollFirst())); + assertThat(kv.key().getInt()).isEqualTo(list.pollFirst()); + assertThat(kv.val().getInt()).isEqualTo(list.pollFirst()); } } } - @Test(expected = IllegalStateException.class) - public void iteratorOnlyReturnedOnce() { - try (Txn txn = env.txnRead(); - CursorIterable c = db.iterate(txn)) { - c.iterator(); // ok - c.iterator(); // fails - } + @Test + void iteratorOnlyReturnedOnce() { + 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() { + void lessThanBackwardTest() { verify(lessThanBackward(bb(5)), 8, 6); verify(lessThanBackward(bb(2)), 8, 6, 4); } @Test - public void lessThanTest() { + void lessThanTest() { verify(lessThan(bb(5)), 2, 4); verify(lessThan(bb(8)), 2, 4, 6); } - @Test(expected = NoSuchElementException.class) - public void nextThrowsNoSuchElementExceptionIfNoMoreElements() { - try (Txn txn = env.txnRead(); - CursorIterable c = db.iterate(txn)) { - final Iterator> i = c.iterator(); - while (i.hasNext()) { - final KeyVal kv = i.next(); - assertThat(kv.key().getInt(), is(list.pollFirst())); - assertThat(kv.val().getInt(), is(list.pollFirst())); - } - assertThat(i.hasNext(), is(false)); - i.next(); - } + @Test + void nextThrowsNoSuchElementExceptionIfNoMoreElements() { + assertThatThrownBy( + () -> { + final Dbi db = getDb(); + populateTestDataList(); + try (Txn txn = env.txnRead(); + CursorIterable c = db.iterate(txn)) { + final Iterator> i = c.iterator(); + while (i.hasNext()) { + final KeyVal kv = i.next(); + assertThat(kv.key().getInt()).isEqualTo(list.pollFirst()); + assertThat(kv.val().getInt()).isEqualTo(list.pollFirst()); + } + assertThat(i.hasNext()).isFalse(); + i.next(); + } + }) + .isInstanceOf(NoSuchElementException.class); } @Test - public void openBackwardTest() { + void openBackwardTest() { verify(openBackward(bb(7), bb(2)), 6, 4); verify(openBackward(bb(8), bb(1)), 6, 4, 2); verify(openBackward(bb(9), bb(4)), 8, 6); } @Test - public void openClosedBackwardTest() { + void openClosedBackwardTest() { verify(openClosedBackward(bb(7), bb(2)), 6, 4, 2); verify(openClosedBackward(bb(8), bb(4)), 6, 4); verify(openClosedBackward(bb(9), bb(4)), 8, 6, 4); } @Test - public void openClosedBackwardTestWithGuava() { + void openClosedBackwardTestWithGuava() { final Comparator guava = UnsignedBytes.lexicographicalComparator(); - final Comparator comparator = (bb1, bb2) -> { - final byte[] array1 = new byte[bb1.remaining()]; - final byte[] array2 = new byte[bb2.remaining()]; - bb1.mark(); - bb2.mark(); - bb1.get(array1); - bb2.get(array2); - bb1.reset(); - bb2.reset(); - return guava.compare(array1, array2); - }; - final Dbi guavaDbi = env.openDbi(DB_1, comparator, MDB_CREATE); + 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) + .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); } @Test - public void openClosedTest() { + void openClosedTest() { verify(openClosed(bb(3), bb(8)), 4, 6, 8); verify(openClosed(bb(2), bb(6)), 4, 6); } @Test - public void openTest() { + void openTest() { verify(open(bb(3), bb(7)), 4, 6); verify(open(bb(2), bb(8)), 4, 6); } @Test - public void removeOddElements() { + void removeOddElements() { + final Dbi db = getDb(); verify(all(), 2, 4, 6, 8); int idx = -1; try (Txn txn = env.txnWrite()) { @@ -306,85 +344,224 @@ public void removeOddElements() { } txn.commit(); } - verify(all(), 4, 8); + verify(db, all(), 4, 8); } - @Test(expected = Env.AlreadyClosedException.class) - public void nextWithClosedEnvTest() { - try (Txn txn = env.txnRead()) { - try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { - final Iterator> c = ci.iterator(); - - env.close(); - c.next(); - } - } + @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(); + + env.close(); + c.next(); + } + } + }) + .isInstanceOf(Env.AlreadyClosedException.class); } - @Test(expected = Env.AlreadyClosedException.class) - public void removeWithClosedEnvTest() { - try (Txn txn = env.txnWrite()) { - try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { - final Iterator> c = ci.iterator(); + @Test + 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(); - final KeyVal keyVal = c.next(); - assertThat(keyVal, Matchers.notNullValue()); + final KeyVal keyVal = c.next(); + assertThat(keyVal).isNotNull(); - env.close(); - c.remove(); - } - } + env.close(); + c.remove(); + } + } + }) + .isInstanceOf(Env.AlreadyClosedException.class); } - @Test(expected = Env.AlreadyClosedException.class) - public void hasNextWithClosedEnvTest() { - try (Txn txn = env.txnRead()) { - try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { - final Iterator> c = ci.iterator(); - - env.close(); - c.hasNext(); - } - } + @Test + 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(); + + env.close(); + c.hasNext(); + } + } + }) + .isInstanceOf(Env.AlreadyClosedException.class); } - @Test(expected = Env.AlreadyClosedException.class) - public void forEachRemainingWithClosedEnvTest() { - try (Txn txn = env.txnRead()) { - try (CursorIterable ci = db.iterate(txn, KeyRange.all())) { - final Iterator> c = ci.iterator(); - - env.close(); - c.forEachRemaining(keyVal -> { - - }); - } - } + @Test + 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(); + + env.close(); + c.forEachRemaining(keyVal -> {}); + } + } + }) + .isInstanceOf(Env.AlreadyClosedException.class); + } + + // @Test + // public void testSignedVsUnsigned() { + // final ByteBuffer val1 = bb(1); + // final ByteBuffer val2 = bb(2); + // final ByteBuffer val110 = bb(110); + // final ByteBuffer val111 = bb(111); + // final ByteBuffer val150 = bb(150); + // + // final BufferProxy bufferProxy = ByteBufferProxy.PROXY_OPTIMAL; + // final Comparator unsignedComparator = bufferProxy.getUnsignedComparator(); + // final Comparator signedComparator = bufferProxy.getSignedComparator(); + // + // // Compare the same + // assertThat( + // unsignedComparator.compare(val1, val2), Matchers.is(signedComparator.compare(val1, + // val2))); + // + // // Compare differently + // assertThat( + // unsignedComparator.compare(val110, val150), + // Matchers.not(signedComparator.compare(val110, val150))); + // + // // Compare differently + // assertThat( + // unsignedComparator.compare(val111, val150), + // Matchers.not(signedComparator.compare(val111, val150))); + // + // // This will fail if the db is using a signed comparator for the start/stop keys + // for (final Dbi db : dbs) { + // db.put(val110, val110); + // db.put(val150, val150); + // + // final ByteBuffer startKeyBuf = val111; + // KeyRange keyRange = KeyRange.atLeastBackward(startKeyBuf); + // + // try (Txn txn = env.txnRead(); + // CursorIterable c = db.iterate(txn, keyRange)) { + // for (final CursorIterable.KeyVal kv : c) { + // final int key = kv.key().getInt(); + // final int val = kv.val().getInt(); + // // System.out.println("key: " + key + " val: " + val); + // assertThat(key, is(110)); + // break; + // } + // } + // } + // } + + private void verify(final KeyRange range, final int... expected) { + final Dbi db = getDb(); + verify(range, db, expected); } - private void verify(final KeyRange range, - final int... expected) { - 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) { + private void verify( + final KeyRange range, final Dbi dbi, final int... expected) { + final List results = new ArrayList<>(); try (Txn txn = env.txnRead(); - CursorIterable c = dbi.iterate(txn, range)) { + CursorIterable c = dbi.iterate(txn, range)) { for (final KeyVal kv : c) { final int key = kv.key().getInt(); final int val = kv.val().getInt(); results.add(key); - assertThat(val, is(key + 1)); + assertThat(val).isEqualTo(key + 1); } } - assertThat(results, hasSize(expected.length)); + assertThat(results.size()).isEqualTo(expected.length); for (int idx = 0; idx < results.size(); idx++) { - assertThat(results.get(idx), is(expected[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(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 b9fd60e3..28f60419 100644 --- a/src/test/java/org/lmdbjava/CursorParamTest.java +++ b/src/test/java/org/lmdbjava/CursorParamTest.java @@ -1,31 +1,24 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ package org.lmdbjava; -import static com.jakewharton.byteunits.BinaryByteUnit.KIBIBYTES; import static java.lang.Long.BYTES; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.collection.IsEmptyCollection.empty; +import static org.assertj.core.api.Assertions.assertThat; import static org.lmdbjava.ByteArrayProxy.PROXY_BA; import static org.lmdbjava.ByteBufProxy.PROXY_NETTY; import static org.lmdbjava.ByteBufferProxy.PROXY_OPTIMAL; @@ -43,53 +36,44 @@ 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; -import java.io.File; -import java.io.IOException; -import java.nio.ByteBuffer; - import io.netty.buffer.ByteBuf; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.util.stream.Stream; import org.agrona.DirectBuffer; import org.agrona.MutableDirectBuffer; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameter; -import org.junit.runners.Parameterized.Parameters; - -/** - * Test {@link Cursor} with different buffer implementations. - */ -@RunWith(Parameterized.class) +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.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 { - /** - * Injected by {@link #data()} with appropriate runner. - */ - @Parameter - public BufferRunner runner; - - @Rule - public final TemporaryFolder tmp = new TemporaryFolder(); - - @Parameters(name = "{index}: buffer adapter: {0}") - public static Object[] data() { - final BufferRunner bb1 = new ByteBufferRunner(PROXY_OPTIMAL); - final BufferRunner bb2 = new ByteBufferRunner(PROXY_SAFE); - final BufferRunner ba = new ByteArrayRunner(PROXY_BA); - final BufferRunner db = new DirectBufferRunner(); - final BufferRunner netty = new NettyBufferRunner(); - return new Object[]{bb1, bb2, ba, db, netty}; + 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())); + } } - @Test - public void execute() { + @ParameterizedTest + @ArgumentsSource(MyArgumentProvider.class) + void execute(final BufferRunner runner, @TempDir final Path tmp) { runner.execute(tmp); } @@ -98,8 +82,7 @@ public void execute() { * * @param buffer type */ - private abstract static class AbstractBufferRunner implements - BufferRunner { + private abstract static class AbstractBufferRunner implements BufferRunner { final BufferProxy proxy; @@ -108,13 +91,18 @@ protected AbstractBufferRunner(final BufferProxy proxy) { } @Override - public final void execute(final TemporaryFolder tmp) { + public final void execute(final Path tmp) { try (Env env = env(tmp)) { - assertThat(env.getDbiNames(), empty()); - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); - assertThat(env.getDbiNames().get(0), is(DB_1.getBytes(UTF_8))); + assertThat(env.getDbiNames()).isEmpty(); + 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)) { + Cursor c = db.openCursor(txn)) { // populate data c.put(set(1), set(2), MDB_NOOVERWRITE); c.put(set(3), set(4)); @@ -126,78 +114,71 @@ public final void execute(final TemporaryFolder tmp) { // check MDB_SET operations final T key3 = set(3); - assertThat(c.get(key3, MDB_SET_KEY), is(true)); - assertThat(get(c.key()), is(3)); - assertThat(get(c.val()), is(4)); + assertThat(c.get(key3, MDB_SET_KEY)).isTrue(); + assertThat(get(c.key())).isEqualTo(3); + assertThat(get(c.val())).isEqualTo(4); final T key6 = set(6); - assertThat(c.get(key6, MDB_SET_RANGE), is(true)); - assertThat(get(c.key()), is(7)); + assertThat(c.get(key6, MDB_SET_RANGE)).isTrue(); + assertThat(get(c.key())).isEqualTo(7); if (!(this instanceof ByteArrayRunner)) { - assertThat(get(c.val()), is(8)); + assertThat(get(c.val())).isEqualTo(8); } final T key999 = set(999); - assertThat(c.get(key999, MDB_SET_KEY), is(false)); + assertThat(c.get(key999, MDB_SET_KEY)).isFalse(); // check MDB navigation operations - assertThat(c.seek(MDB_LAST), is(true)); + assertThat(c.seek(MDB_LAST)).isTrue(); final int mdb1 = get(c.key()); final int mdb2 = get(c.val()); - assertThat(c.seek(MDB_PREV), is(true)); + assertThat(c.seek(MDB_PREV)).isTrue(); final int mdb3 = get(c.key()); final int mdb4 = get(c.val()); - assertThat(c.seek(MDB_NEXT), is(true)); + assertThat(c.seek(MDB_NEXT)).isTrue(); final int mdb5 = get(c.key()); final int mdb6 = get(c.val()); - assertThat(c.seek(MDB_FIRST), is(true)); + assertThat(c.seek(MDB_FIRST)).isTrue(); final int mdb7 = get(c.key()); final int mdb8 = get(c.val()); // assert afterwards to ensure memory address from LMDB // are valid within same txn and across cursor movement // MDB_LAST - assertThat(mdb1, is(7)); + assertThat(mdb1).isEqualTo(7); if (!(this instanceof ByteArrayRunner)) { - assertThat(mdb2, is(8)); + assertThat(mdb2).isEqualTo(8); } // MDB_PREV - assertThat(mdb3, is(5)); - assertThat(mdb4, is(6)); + assertThat(mdb3).isEqualTo(5); + assertThat(mdb4).isEqualTo(6); // MDB_NEXT - assertThat(mdb5, is(7)); + assertThat(mdb5).isEqualTo(7); if (!(this instanceof ByteArrayRunner)) { - assertThat(mdb6, is(8)); + assertThat(mdb6).isEqualTo(8); } // MDB_FIRST - assertThat(mdb7, is(1)); - assertThat(mdb8, is(2)); + assertThat(mdb7).isEqualTo(1); + assertThat(mdb8).isEqualTo(2); } } } - private Env env(final TemporaryFolder tmp) { - try { - final File path = tmp.newFile(); - return create(proxy) - .setMapSize(KIBIBYTES.toBytes(1_024)) - .setMaxReaders(1) - .setMaxDbs(1) - .open(path, POSIX_MODE, MDB_NOSUBDIR); - } catch (final IOException e) { - throw new LmdbException("IO failure", e); - } + private Env env(final Path tmp) { + return create(proxy) + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxReaders(1) + .setMaxDbs(1) + .setEnvFlags(MDB_NOSUBDIR) + .open(tmp.resolve("db")); } - } - /** - * {@link BufferRunner} for Java byte buffers. - */ + /** {@link BufferRunner} for Java byte buffers. */ private static class ByteArrayRunner extends AbstractBufferRunner { ByteArrayRunner(final BufferProxy proxy) { @@ -207,9 +188,9 @@ private static class ByteArrayRunner extends AbstractBufferRunner { @Override public int get(final byte[] buff) { return (buff[0] & 0xFF) << 24 - | (buff[1] & 0xFF) << 16 - | (buff[2] & 0xFF) << 8 - | (buff[3] & 0xFF); + | (buff[1] & 0xFF) << 16 + | (buff[2] & 0xFF) << 8 + | (buff[3] & 0xFF); } @Override @@ -231,9 +212,7 @@ public void set(final byte[] buff, final int val) { } } - /** - * {@link BufferRunner} for Java byte buffers. - */ + /** {@link BufferRunner} for Java byte buffers. */ private static class ByteBufferRunner extends AbstractBufferRunner { ByteBufferRunner(final BufferProxy proxy) { @@ -254,12 +233,9 @@ public ByteBuffer set(final int val) { public void set(final ByteBuffer buff, final int val) { buff.putInt(val); } - } - /** - * {@link BufferRunner} for Agrona direct buffer. - */ + /** {@link BufferRunner} for Agrona direct buffer. */ private static class DirectBufferRunner extends AbstractBufferRunner { DirectBufferRunner() { @@ -280,12 +256,9 @@ public DirectBuffer set(final int val) { public void set(final DirectBuffer buff, final int val) { ((MutableDirectBuffer) buff).putInt(0, val); } - } - /** - * {@link BufferRunner} for Netty byte buf. - */ + /** {@link BufferRunner} for Netty byte buf. */ private static class NettyBufferRunner extends AbstractBufferRunner { NettyBufferRunner() { @@ -306,7 +279,6 @@ public ByteBuf set(final int val) { public void set(final ByteBuf buff, final int val) { buff.setInt(0, val); } - } /** @@ -316,7 +288,7 @@ public void set(final ByteBuf buff, final int val) { */ private interface BufferRunner { - void execute(TemporaryFolder tmp); + void execute(Path tmp); T set(int val); @@ -324,5 +296,4 @@ private interface BufferRunner { int get(T buff); } - } diff --git a/src/test/java/org/lmdbjava/CursorTest.java b/src/test/java/org/lmdbjava/CursorTest.java index 17fbacc7..deb75622 100644 --- a/src/test/java/org/lmdbjava/CursorTest.java +++ b/src/test/java/org/lmdbjava/CursorTest.java @@ -1,33 +1,26 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ package org.lmdbjava; -import static com.jakewharton.byteunits.BinaryByteUnit.KIBIBYTES; import static java.lang.Long.BYTES; import static java.lang.Long.MIN_VALUE; import static java.nio.ByteBuffer.allocateDirect; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.lmdbjava.ByteBufferProxy.PROXY_OPTIMAL; import static org.lmdbjava.DbiFlags.MDB_CREATE; import static org.lmdbjava.DbiFlags.MDB_DUPFIXED; @@ -43,198 +36,329 @@ import static org.lmdbjava.SeekOp.MDB_LAST; import static org.lmdbjava.SeekOp.MDB_NEXT; import static org.lmdbjava.TestUtils.DB_1; -import static org.lmdbjava.TestUtils.POSIX_MODE; import static org.lmdbjava.TestUtils.bb; -import java.io.File; -import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.file.Path; import java.util.function.Consumer; - -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; +import org.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.Cursor.ClosedException; +import org.lmdbjava.Env.AlreadyClosedException; import org.lmdbjava.Txn.NotReadyException; import org.lmdbjava.Txn.ReadOnlyRequiredException; -/** - * Test {@link Cursor}. - */ +/** Test {@link Cursor}. */ public final class CursorTest { - @Rule - public final TemporaryFolder tmp = new TemporaryFolder(); - private Env env; - - @After - public void after() { - env.close(); + private TempDir tempDir; + + @BeforeEach + void beforeEach() { + tempDir = new TempDir(); + Path file = tempDir.createTempFile(); + env = + create(PROXY_OPTIMAL) + .setMapSize(1, ByteUnit.MEBIBYTES) + .setMaxReaders(1) + .setMaxDbs(1) + .setEnvFlags(MDB_NOSUBDIR) + .open(file); } - @Before - public void before() throws IOException { - try { - final File path = tmp.newFile(); - env = create(PROXY_OPTIMAL) - .setMapSize(KIBIBYTES.toBytes(1_024)) - .setMaxReaders(1) - .setMaxDbs(1) - .open(path, POSIX_MODE, MDB_NOSUBDIR); - } catch (final IOException e) { - throw new LmdbException("IO failure", e); - } + @AfterEach + void afterEach() { + env.close(); + tempDir.cleanup(); } - @Test(expected = ClosedException.class) - public void closedCursorRejectsSubsequentGets() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - try (Txn txn = env.txnWrite()) { - final Cursor c = db.openCursor(txn); - c.close(); - c.seek(MDB_FIRST); - } + @Test + void closedCursorRejectsSubsequentGets() { + assertThatThrownBy( + () -> { + 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(); + c.seek(MDB_FIRST); + } + }) + .isInstanceOf(ClosedException.class); } - @Test(expected = Env.AlreadyClosedException.class) - public void closedEnvRejectsSeekFirstCall() { - doEnvClosedTest(null, c -> c.seek(MDB_FIRST)); + @Test + void closedEnvRejectsSeekFirstCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, c -> c.seek(MDB_FIRST)); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = Env.AlreadyClosedException.class) - public void closedEnvRejectsSeekLastCall() { - doEnvClosedTest(null, c -> c.seek(MDB_LAST)); + @Test + void closedEnvRejectsSeekLastCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, c -> c.seek(MDB_LAST)); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = Env.AlreadyClosedException.class) - public void closedEnvRejectsSeekNextCall() { - doEnvClosedTest(null, c -> c.seek(MDB_NEXT)); + @Test + void closedEnvRejectsSeekNextCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, c -> c.seek(MDB_NEXT)); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = Env.AlreadyClosedException.class) - public void closedEnvRejectsCloseCall() { - doEnvClosedTest(null, Cursor::close); + @Test + void closedEnvRejectsCloseCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, Cursor::close); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = Env.AlreadyClosedException.class) - public void closedEnvRejectsFirstCall() { - doEnvClosedTest(null, Cursor::first); + @Test + void closedEnvRejectsFirstCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, Cursor::first); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = Env.AlreadyClosedException.class) - public void closedEnvRejectsLastCall() { - doEnvClosedTest(null, Cursor::last); + @Test + void closedEnvRejectsLastCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, Cursor::last); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = Env.AlreadyClosedException.class) - public void closedEnvRejectsPrevCall() { - doEnvClosedTest( - c -> { - c.first(); - assertThat(c.key().getInt(), is(1)); - assertThat(c.val().getInt(), is(10)); - c.next(); - }, - Cursor::prev); + @Test + void closedEnvRejectsPrevCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest( + c -> { + c.first(); + assertThat(c.key().getInt()).isEqualTo(1); + assertThat(c.val().getInt()).isEqualTo(10); + c.next(); + }, + Cursor::prev); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = Env.AlreadyClosedException.class) - public void closedEnvRejectsDeleteCall() { - doEnvClosedTest( - c -> { - c.first(); - assertThat(c.key().getInt(), is(1)); - assertThat(c.val().getInt(), is(10)); - }, - Cursor::delete); + @Test + void closedEnvRejectsDeleteCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest( + c -> { + c.first(); + assertThat(c.key().getInt()).isEqualTo(1); + assertThat(c.val().getInt()).isEqualTo(10); + }, + Cursor::delete); + }) + .isInstanceOf(AlreadyClosedException.class); } @Test - public 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)) { + Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_APPENDDUP); - assertThat(c.count(), is(1L)); + assertThat(c.count()).isEqualTo(1L); c.put(bb(1), bb(4), MDB_APPENDDUP); c.put(bb(1), bb(6), MDB_APPENDDUP); - assertThat(c.count(), is(3L)); + assertThat(c.count()).isEqualTo(3L); c.put(bb(2), bb(1), MDB_APPENDDUP); c.put(bb(2), bb(2), MDB_APPENDDUP); - assertThat(c.count(), is(2L)); + assertThat(c.count()).isEqualTo(2L); } } - @Test(expected = NotReadyException.class) - public void cursorCannotCloseIfTransactionCommitted() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); - try (Txn txn = env.txnWrite()) { - try (Cursor c = db.openCursor(txn);) { - c.put(bb(1), bb(2), MDB_APPENDDUP); - assertThat(c.count(), is(1L)); - c.put(bb(1), bb(4), MDB_APPENDDUP); - assertThat(c.count(), is(2L)); - txn.commit(); - } + @Test + 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 - public void cursorFirstLastNextPrev() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + void cursorCannotCloseIfTransactionCommitted() { + assertThatThrownBy( + () -> { + 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); + assertThat(c.count()).isEqualTo(1L); + c.put(bb(1), bb(4), MDB_APPENDDUP); + assertThat(c.count()).isEqualTo(2L); + txn.commit(); + } + } + }) + .isInstanceOf(NotReadyException.class); + } + + @Test + void cursorFirstLastNextPrev() { + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_NOOVERWRITE); c.put(bb(3), bb(4)); c.put(bb(5), bb(6)); c.put(bb(7), bb(8)); - assertThat(c.first(), is(true)); - assertThat(c.key().getInt(0), is(1)); - assertThat(c.val().getInt(0), is(2)); + assertThat(c.first()).isTrue(); + assertThat(c.key().getInt(0)).isEqualTo(1); + assertThat(c.val().getInt(0)).isEqualTo(2); - assertThat(c.last(), is(true)); - assertThat(c.key().getInt(0), is(7)); - assertThat(c.val().getInt(0), is(8)); + assertThat(c.last()).isTrue(); + assertThat(c.key().getInt(0)).isEqualTo(7); + assertThat(c.val().getInt(0)).isEqualTo(8); - assertThat(c.prev(), is(true)); - assertThat(c.key().getInt(0), is(5)); - assertThat(c.val().getInt(0), is(6)); + assertThat(c.prev()).isTrue(); + assertThat(c.key().getInt(0)).isEqualTo(5); + assertThat(c.val().getInt(0)).isEqualTo(6); - assertThat(c.first(), is(true)); - assertThat(c.next(), is(true)); - assertThat(c.key().getInt(0), is(3)); - assertThat(c.val().getInt(0), is(4)); + assertThat(c.first()).isTrue(); + assertThat(c.next()).isTrue(); + assertThat(c.key().getInt(0)).isEqualTo(3); + assertThat(c.val().getInt(0)).isEqualTo(4); } } @Test - public void delete() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + void delete() { + 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)) { + Cursor c = db.openCursor(txn)) { c.put(bb(1), bb(2), MDB_NOOVERWRITE); c.put(bb(3), bb(4)); - assertThat(c.seek(MDB_FIRST), is(true)); - assertThat(c.key().getInt(), is(1)); - assertThat(c.val().getInt(), is(2)); + assertThat(c.seek(MDB_FIRST)).isTrue(); + assertThat(c.key().getInt()).isEqualTo(1); + assertThat(c.val().getInt()).isEqualTo(2); c.delete(); - assertThat(c.seek(MDB_FIRST), is(true)); - assertThat(c.key().getInt(), is(3)); - assertThat(c.val().getInt(), is(4)); + assertThat(c.seek(MDB_FIRST)).isTrue(); + assertThat(c.key().getInt()).isEqualTo(3); + assertThat(c.val().getInt()).isEqualTo(4); c.delete(); - assertThat(c.seek(MDB_FIRST), is(false)); + assertThat(c.seek(MDB_FIRST)).isFalse(); + } + } + + @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 - public void getKeyVal() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + 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)) { + 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.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); c.put(bb(1), bb(4), MDB_APPENDDUP); c.put(bb(1), bb(6), MDB_APPENDDUP); @@ -242,19 +366,23 @@ public void getKeyVal() { c.put(bb(2), bb(2), MDB_APPENDDUP); c.put(bb(2), bb(3), MDB_APPENDDUP); c.put(bb(2), bb(4), MDB_APPENDDUP); - assertThat(c.get(bb(1), bb(2), MDB_GET_BOTH), is(true)); - assertThat(c.count(), is(3L)); - assertThat(c.get(bb(1), bb(3), MDB_GET_BOTH), is(false)); - assertThat(c.get(bb(2), bb(1), MDB_GET_BOTH), is(true)); - assertThat(c.count(), is(4L)); - assertThat(c.get(bb(2), bb(0), MDB_GET_BOTH), is(false)); + 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 - public void putMultiple() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT, - MDB_DUPFIXED); + void putMultiple() { + 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); @@ -266,24 +394,70 @@ public void putMultiple() { final int key = 100; final ByteBuffer k = bb(key); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { c.putMultiple(k, values, elemCount, MDB_MULTIPLE); - assertThat(c.count(), is((long) elemCount)); + assertThat(c.count()).isEqualTo((long) elemCount); } } - @Test(expected = IllegalArgumentException.class) - public void putMultipleWithoutMdbMultipleFlag() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + @Test + void putMultipleWithoutMdbMultipleFlag() { + 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.putMultiple(bb(100), bb(1), 1); + Cursor c = db.openCursor(txn)) { + assertThatThrownBy( + () -> { + c.putMultiple(bb(100), bb(1), 1); + }) + .isInstanceOf(IllegalArgumentException.class); } } @Test - public void renewTxRo() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + 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.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); final Cursor c; try (Txn txn = env.txnRead()) { @@ -299,21 +473,35 @@ public void renewTxRo() { c.close(); } - @Test(expected = ReadOnlyRequiredException.class) - public void renewTxRw() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - try (Txn txn = env.txnWrite()) { - assertThat(txn.isReadOnly(), is(false)); - - try (Cursor c = db.openCursor(txn)) { - c.renew(txn); - } - } + @Test + void renewTxRw() { + assertThatThrownBy( + () -> { + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + try (Txn txn = env.txnWrite()) { + assertThat(txn.isReadOnly()).isFalse(); + + try (Cursor c = db.openCursor(txn)) { + c.renew(txn); + } + } + }) + .isInstanceOf(ReadOnlyRequiredException.class); } @Test - public void repeatedCloseCausesNotError() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + void repeatedCloseCausesNotError() { + 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(); @@ -322,53 +510,61 @@ public void repeatedCloseCausesNotError() { } @Test - public void reserve() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + void reserve() { + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); final ByteBuffer key = bb(5); try (Txn txn = env.txnWrite()) { - assertNull(db.get(txn, key)); + assertThat(db.get(txn, key)).isNull(); try (Cursor c = db.openCursor(txn)) { final ByteBuffer val = c.reserve(key, BYTES * 2); - assertNotNull(db.get(txn, key)); + 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(), is(BYTES * 2)); - assertThat(val.getLong(), is(MIN_VALUE)); + assertThat(val.capacity()).isEqualTo(BYTES * 2); + assertThat(val.getLong()).isEqualTo(MIN_VALUE); } } @Test - public void returnValueForNoDupData() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + void returnValueForNoDupData() { + 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)) { + Cursor c = db.openCursor(txn)) { // ok - assertThat(c.put(bb(5), bb(6), MDB_NODUPDATA), is(true)); - assertThat(c.put(bb(5), bb(7), MDB_NODUPDATA), is(true)); - assertThat(c.put(bb(5), bb(6), MDB_NODUPDATA), is(false)); + assertThat(c.put(bb(5), bb(6), MDB_NODUPDATA)).isTrue(); + assertThat(c.put(bb(5), bb(7), MDB_NODUPDATA)).isTrue(); + assertThat(c.put(bb(5), bb(6), MDB_NODUPDATA)).isFalse(); } } @Test - public void returnValueForNoOverwrite() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + void returnValueForNoOverwrite() { + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); try (Txn txn = env.txnWrite(); - Cursor c = db.openCursor(txn)) { + Cursor c = db.openCursor(txn)) { // ok - assertThat(c.put(bb(5), bb(6), MDB_NOOVERWRITE), is(true)); + assertThat(c.put(bb(5), bb(6), MDB_NOOVERWRITE)).isTrue(); // fails, but gets exist val - assertThat(c.put(bb(5), bb(8), MDB_NOOVERWRITE), is(false)); - assertThat(c.val().getInt(0), is(6)); + assertThat(c.put(bb(5), bb(8), MDB_NOOVERWRITE)).isFalse(); + assertThat(c.val().getInt(0)).isEqualTo(6); } } @Test - public void testCursorByteBufferDuplicate() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + void testCursorByteBufferDuplicate() { + 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)); @@ -386,18 +582,20 @@ public void testCursorByteBufferDuplicate() { final ByteBuffer key2 = c.key().duplicate(); final ByteBuffer val2 = c.val().duplicate(); - assertThat(key1.getInt(0), is(1)); - assertThat(val1.getInt(0), is(2)); + assertThat(key1.getInt(0)).isEqualTo(1); + assertThat(val1.getInt(0)).isEqualTo(2); - assertThat(key2.getInt(0), is(3)); - assertThat(val2.getInt(0), is(4)); + assertThat(key2.getInt(0)).isEqualTo(3); + assertThat(val2.getInt(0)).isEqualTo(4); } } } - private void doEnvClosedTest(final Consumer> workBeforeEnvClosed, - final Consumer> workAfterEnvClose) { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + private void doEnvClosedTest( + final Consumer> workBeforeEnvClosed, + final Consumer> workAfterEnvClose) { + 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 5a8cf549..575937f5 100644 --- a/src/test/java/org/lmdbjava/DbiTest.java +++ b/src/test/java/org/lmdbjava/DbiTest.java @@ -1,26 +1,21 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ package org.lmdbjava; -import static 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,16 +24,8 @@ import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.stream.Collectors.toList; import static java.util.stream.IntStream.range; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.collection.IsEmptyCollection.empty; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; +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.DbiFlags.MDB_CREATE; @@ -54,10 +41,10 @@ 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.io.File; -import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Comparator; import java.util.Iterator; @@ -69,120 +56,276 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; - -import org.agrona.concurrent.UnsafeBuffer; -import org.hamcrest.Matchers; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; +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; +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; -/** - * Test {@link Dbi}. - */ +/** Test {@link Dbi}. */ public final class DbiTest { - @Rule - public final TemporaryFolder tmp = new TemporaryFolder(); + private TempDir tempDir; private Env env; - - @After - public void after() { + private Env envBa; + + @BeforeEach + void beforeEach() { + tempDir = new TempDir(); + final Path file = tempDir.createTempFile(); + env = + create() + .setMapSize(64, ByteUnit.MEBIBYTES) + .setMaxReaders(2) + .setMaxDbs(2) + .setEnvFlags(MDB_NOSUBDIR) + .open(file); + final Path fileBa = tempDir.createTempFile(); + envBa = + create(PROXY_BA) + .setMapSize(64, ByteUnit.MEBIBYTES) + .setMaxReaders(2) + .setMaxDbs(2) + .setEnvFlags(MDB_NOSUBDIR) + .open(fileBa); + } + + @AfterEach + void afterEach() { env.close(); + envBa.close(); + tempDir.cleanup(); } - @Before - public void before() throws IOException { - final File path = tmp.newFile(); - env = create() - .setMapSize(MEBIBYTES.toBytes(64)) - .setMaxReaders(2) - .setMaxDbs(2) - .open(path, MDB_NOSUBDIR); + @Test + void close() { + assertThatThrownBy( + () -> { + 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 + }) + .isInstanceOf(ConstantDerivedException.class); } - @Test(expected = ConstantDerivedException.class) - public void close() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - db.put(bb(1), bb(42)); - db.close(); - db.put(bb(2), bb(42)); // error + @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 - public void customComparator() { - final Comparator reverseOrder = (o1, o2) -> { - final int lexical = PROXY_OPTIMAL.getComparator().compare(o1, o2); - if (lexical == 0) { - return 0; - } - return lexical * -1; - }; - final Dbi db = env.openDbi(DB_1, reverseOrder, true, MDB_CREATE); - try (Txn txn = env.txnWrite()) { - assertThat(db.put(txn, bb(2), bb(3)), is(true)); - assertThat(db.put(txn, bb(4), bb(6)), is(true)); - assertThat(db.put(txn, bb(6), bb(7)), is(true)); - assertThat(db.put(txn, bb(8), bb(7)), is(true)); + 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 = + (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.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(); + 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(bb(4)))) { - final Iterator> iter = ci.iterator(); - assertThat(iter.next().key().getInt(), is(8)); - assertThat(iter.next().key().getInt(), is(6)); - assertThat(iter.next().key().getInt(), is(4)); + 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(expected = DbFullException.class) - @SuppressWarnings("ResultOfObjectAllocationIgnored") - public void dbOpenMaxDatabases() { - env.openDbi("db1 is OK", MDB_CREATE); - env.openDbi("db2 is OK", MDB_CREATE); - env.openDbi("db3 fails", MDB_CREATE); + @Test + void dbOpenMaxDatabases() { + assertThatThrownBy( + () -> { + 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); } @Test - public void dbiWithComparatorThreadSafety() { - final DbiFlags[] flags = new DbiFlags[] {MDB_CREATE, MDB_INTEGERKEY}; - final Comparator c = PROXY_OPTIMAL.getComparator(flags); - final Dbi db = env.openDbi(DB_1, c, true, flags); + 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, + Supplier> comparatorSupplier, + IntFunction serializer, + ToIntFunction deserializer) { + 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(() -> { - while (proceed.get()) { - try (Txn txn = env.txnRead()) { - db.get(txn, bb(50)); - } - } - }); + 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, bb(key), bb(3)); + 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(); + 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(iter.next().key().getInt()); + result.add(deserializer.applyAsInt(iter.next().key())); } - assertThat(result, Matchers.contains(keys.toArray(new Integer[0]))); + assertThat(result).contains(keys.toArray(new Integer[0])); } proceed.set(false); @@ -196,51 +339,58 @@ public void dbiWithComparatorThreadSafety() { } @Test - public void drop() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + void drop() { + 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)); - assertThat(db.get(txn, bb(1)), not(nullValue())); - assertThat(db.get(txn, bb(2)), not(nullValue())); + assertThat(db.get(txn, bb(1))).isNotNull(); + assertThat(db.get(txn, bb(2))).isNotNull(); db.drop(txn); - assertThat(db.get(txn, bb(1)), is(nullValue())); // data gone - assertThat(db.get(txn, bb(2)), is(nullValue())); + 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)), not(nullValue())); - assertThat(db.get(txn, bb(2)), not(nullValue())); + assertThat(db.get(txn, bb(1))).isNotNull(); + assertThat(db.get(txn, bb(2))).isNotNull(); } } @Test - public void dropAndDelete() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - final Dbi nameDb = env.openDbi((byte[]) null); + void dropAndDelete() { + 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(); try (Txn txn = env.txnWrite()) { - assertThat(nameDb.get(txn, dbNameBuffer), not(nullValue())); + assertThat(nameDb.get(txn, dbNameBuffer)).isNotNull(); db.drop(txn, true); - assertThat(nameDb.get(txn, dbNameBuffer), is(nullValue())); + assertThat(nameDb.get(txn, dbNameBuffer)).isNull(); txn.commit(); } } @Test - public void dropAndDeleteAnonymousDb() { - env.openDbi(DB_1, MDB_CREATE); - final Dbi nameDb = env.openDbi((byte[]) null); + void dropAndDeleteAnonymousDb() { + 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(); try (Txn txn = env.txnWrite()) { - assertThat(nameDb.get(txn, dbNameBuffer), not(nullValue())); + assertThat(nameDb.get(txn, dbNameBuffer)).isNotNull(); nameDb.drop(txn, true); - assertThat(nameDb.get(txn, dbNameBuffer), is(nullValue())); + assertThat(nameDb.get(txn, dbNameBuffer)).isNull(); txn.commit(); } @@ -248,43 +398,49 @@ public void dropAndDeleteAnonymousDb() { } @Test - public void getName() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - assertThat(db.getName(), is(DB_1.getBytes(UTF_8))); + void getName() { + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + assertThat(db.getName()).isEqualTo(DB_1.getBytes(UTF_8)); } @Test - public 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); + void getNamesWhenDbisPresent() { + final byte[] dbHello = new byte[] {'h', 'e', 'l', 'l', 'o'}; + final byte[] dbWorld = new byte[] {'w', 'o', 'r', 'l', 'd'}; + 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), is(dbHello)); - assertThat(dbiNames.get(1), is(dbWorld)); + assertThat(dbiNames).hasSize(2); + assertThat(dbiNames.get(0)).isEqualTo(dbHello); + assertThat(dbiNames.get(1)).isEqualTo(dbWorld); } @Test - public void getNamesWhenEmpty() { + void getNamesWhenEmpty() { final List dbiNames = env.getDbiNames(); - assertThat(dbiNames, empty()); + assertThat(dbiNames).isEmpty(); } @Test - public void listsFlags() { - final Dbi dbi = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT, - MDB_REVERSEKEY); + void listsFlags() { + 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); - assertThat(flags, containsInAnyOrder(MDB_DUPSORT, MDB_REVERSEKEY)); + assertThat(flags).containsExactlyInAnyOrder(MDB_DUPSORT, MDB_REVERSEKEY); } } @Test - public void putAbortGet() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + void putAbortGet() { + 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)); @@ -292,31 +448,33 @@ public void putAbortGet() { } try (Txn txn = env.txnWrite()) { - assertNull(db.get(txn, bb(5))); + assertThat(db.get(txn, bb(5))).isNull(); } } @Test - public void putAndGetAndDeleteWithInternalTx() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + void putAndGetAndDeleteWithInternalTx() { + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); db.put(bb(5), bb(5)); try (Txn txn = env.txnRead()) { final ByteBuffer found = db.get(txn, bb(5)); - assertNotNull(found); - assertThat(txn.val().getInt(), is(5)); + assertThat(found).isNotNull(); + assertThat(txn.val().getInt()).isEqualTo(5); } - assertThat(db.delete(bb(5)), is(true)); - assertThat(db.delete(bb(5)), is(false)); + assertThat(db.delete(bb(5))).isTrue(); + assertThat(db.delete(bb(5))).isFalse(); try (Txn txn = env.txnRead()) { - assertNull(db.get(txn, bb(5))); + assertThat(db.get(txn, bb(5))).isNull(); } } @Test - public void putCommitGet() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + void putCommitGet() { + 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(); @@ -324,90 +482,102 @@ public void putCommitGet() { try (Txn txn = env.txnWrite()) { final ByteBuffer found = db.get(txn, bb(5)); - assertNotNull(found); - assertThat(txn.val().getInt(), is(5)); + assertThat(found).isNotNull(); + assertThat(txn.val().getInt()).isEqualTo(5); } } @Test - public void putCommitGetByteArray() throws IOException { - final File path = tmp.newFile(); - try (Env envBa = create(PROXY_BA) - .setMapSize(MEBIBYTES.toBytes(64)) - .setMaxReaders(1) - .setMaxDbs(2) - .open(path, MDB_NOSUBDIR)) { - final Dbi db = envBa.openDbi(DB_1, MDB_CREATE); + void putCommitGetByteArray() { + 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)); - assertNotNull(found); - assertThat(new UnsafeBuffer(txn.val()).getInt(0), is(5)); + assertThat(found).isNotNull(); + assertThat(fromBa(txn.val())).isEqualTo(5); } } } @Test - public void putDelete() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + void putDelete() { + 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)); - assertThat(db.delete(txn, bb(5)), is(true)); + assertThat(db.delete(txn, bb(5))).isTrue(); - assertNull(db.get(txn, bb(5))); + assertThat(db.get(txn, bb(5))).isNull(); txn.abort(); } } @Test - public void putDuplicateDelete() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + void putDuplicateDelete() { + 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)); db.put(txn, bb(5), bb(6)); db.put(txn, bb(5), bb(7)); - assertThat(db.delete(txn, bb(5), bb(6)), is(true)); - assertThat(db.delete(txn, bb(5), bb(6)), is(false)); - assertThat(db.delete(txn, bb(5), bb(5)), is(true)); - assertThat(db.delete(txn, bb(5), bb(5)), is(false)); + 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(), is(1L)); + assertThat(cursor.count()).isEqualTo(1L); } txn.abort(); } } @Test - public void putReserve() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + void putReserve() { + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); final ByteBuffer key = bb(5); try (Txn txn = env.txnWrite()) { - assertNull(db.get(txn, key)); + assertThat(db.get(txn, key)).isNull(); final ByteBuffer val = db.reserve(txn, key, 32, MDB_NOOVERWRITE); val.putLong(MAX_VALUE); - assertNotNull(db.get(txn, key)); + assertThat(db.get(txn, key)).isNotNull(); txn.commit(); } try (Txn txn = env.txnWrite()) { final ByteBuffer val = db.get(txn, key); - assertThat(val.capacity(), is(32)); - assertThat(val.getLong(), is(MAX_VALUE)); - assertThat(val.getLong(8), is(0L)); + assertThat(val).isNotNull(); + assertThat(val.capacity()).isEqualTo(32); + assertThat(val.getLong()).isEqualTo(MAX_VALUE); + assertThat(val.getLong(8)).isEqualTo(0L); } } @Test - public void putZeroByteValueForNonMdbDupSortDatabase() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + void putZeroByteValueForNonMdbDupSortDatabase() { + 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); @@ -416,37 +586,44 @@ public void putZeroByteValueForNonMdbDupSortDatabase() { try (Txn txn = env.txnRead()) { final ByteBuffer found = db.get(txn, bb(5)); - assertNotNull(found); - assertThat(txn.val().capacity(), is(0)); + assertThat(found).isNotNull(); + assertThat(txn.val().capacity()).isEqualTo(0); } } @Test - public void returnValueForNoDupData() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE, MDB_DUPSORT); + void returnValueForNoDupData() { + 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), is(true)); - assertThat(db.put(txn, bb(5), bb(7), MDB_NODUPDATA), is(true)); - assertThat(db.put(txn, bb(5), bb(6), MDB_NODUPDATA), is(false)); + 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 - public void returnValueForNoOverwrite() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + void returnValueForNoOverwrite() { + 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), is(true)); + 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), is(false)); - assertThat(txn.val().getInt(0), is(6)); + assertThat(db.put(txn, bb(5), bb(8), MDB_NOOVERWRITE)).isFalse(); + assertThat(txn.val().getInt(0)).isEqualTo(6); } } @Test - public void stats() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + void stats() { + 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)); @@ -454,129 +631,177 @@ public void stats() { try (Txn txn = env.txnRead()) { stat = db.stat(txn); } - assertThat(stat, is(notNullValue())); - assertThat(stat.branchPages, is(0L)); - assertThat(stat.depth, is(1)); - assertThat(stat.entries, is(3L)); - assertThat(stat.leafPages, is(1L)); - assertThat(stat.overflowPages, is(0L)); - assertThat(stat.pageSize % 4_096, is(0)); - } - - @Test(expected = MapFullException.class) - @SuppressWarnings("PMD.PreserveStackTrace") - public void testMapFullException() { - 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); - } + 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 - public void testParallelWritesStress() { + void testMapFullException() { + assertThatThrownBy( + () -> { + final Dbi db = + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + 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); + 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() - .forEach(ignored -> { - for (int i = 0; i < 15_000; i++) { - db.put(bb(i), bb(i)); - } - }); + .forEach( + ignored -> { + for (int i = 0; i < 15_000; i++) { + db.put(bb(i), bb(i)); + } + }); } - @Test(expected = AlreadyClosedException.class) - public void closedEnvRejectsOpenCall() { - env.close(); - env.openDbi(DB_1, MDB_CREATE); + @Test + void closedEnvRejectsOpenCall() { + assertThatThrownBy( + () -> { + env.close(); + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = AlreadyClosedException.class) - public void closedEnvRejectsCloseCall() { - doEnvClosedTest( - null, - (db, txn) -> db.close()); + @Test + void closedEnvRejectsCloseCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, (db, txn) -> db.close()); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = AlreadyClosedException.class) - public void closedEnvRejectsGetCall() { - doEnvClosedTest( - (db, txn) -> { - final ByteBuffer valBuf = db.get(txn, bb(1)); - assertThat(valBuf.getInt(), is(10)); - }, - (db, txn) -> db.get(txn, bb(2))); + @Test + void closedEnvRejectsGetCall() { + assertThatThrownBy( + () -> { + 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))); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = AlreadyClosedException.class) - public void closedEnvRejectsPutCall() { - doEnvClosedTest( - null, - (db, txn) -> db.put(bb(5), bb(50))); + @Test + void closedEnvRejectsPutCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, (db, txn) -> db.put(bb(5), bb(50))); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = AlreadyClosedException.class) - public void closedEnvRejectsPutWithTxnCall() { - doEnvClosedTest( - null, - (db, txn) -> { - db.put(txn, bb(5), bb(50)); - }); + @Test + void closedEnvRejectsPutWithTxnCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest( + null, + (db, txn) -> { + db.put(txn, bb(5), bb(50)); + }); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = AlreadyClosedException.class) - public void closedEnvRejectsIterateCall() { - doEnvClosedTest(null, Dbi::iterate); + @Test + void closedEnvRejectsIterateCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, Dbi::iterate); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = AlreadyClosedException.class) - public void closedEnvRejectsDropCall() { - doEnvClosedTest( - null, - Dbi::drop); + @Test + void closedEnvRejectsDropCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, Dbi::drop); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = AlreadyClosedException.class) - public void closedEnvRejectsDropAndDeleteCall() { - doEnvClosedTest( - null, - (db, txn) -> db.drop(txn, true)); + @Test + void closedEnvRejectsDropAndDeleteCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, (db, txn) -> db.drop(txn, true)); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = AlreadyClosedException.class) - public void closedEnvRejectsOpenCursorCall() { - doEnvClosedTest( - null, - Dbi::openCursor); + @Test + void closedEnvRejectsOpenCursorCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, Dbi::openCursor); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = AlreadyClosedException.class) - public void closedEnvRejectsReserveCall() { - doEnvClosedTest( - null, - (db, txn) -> db.reserve(txn, bb(1), 32, MDB_NOOVERWRITE)); + @Test + void closedEnvRejectsReserveCall() { + assertThatThrownBy( + () -> { + doEnvClosedTest(null, (db, txn) -> db.reserve(txn, bb(1), 32, MDB_NOOVERWRITE)); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = AlreadyClosedException.class) - public void closedEnvRejectsStatCall() { - doEnvClosedTest(null, Dbi::stat); + @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); + 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)); @@ -597,4 +822,11 @@ 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 46d8766f..69a5ea4e 100644 --- a/src/test/java/org/lmdbjava/EnvTest.java +++ b/src/test/java/org/lmdbjava/EnvTest.java @@ -1,52 +1,48 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ package org.lmdbjava; -import static com.jakewharton.byteunits.BinaryByteUnit.KIBIBYTES; -import static com.jakewharton.byteunits.BinaryByteUnit.MEBIBYTES; import static java.nio.ByteBuffer.allocateDirect; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.greaterThan; +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; -import java.io.File; 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.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Random; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; +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; import org.lmdbjava.Env.Builder; @@ -54,295 +50,413 @@ import org.lmdbjava.Env.MapFullException; import org.lmdbjava.Txn.BadReaderLockException; -/** - * Test {@link Env}. - */ +/** Test {@link Env}. */ public final class EnvTest { - @Rule - public final TemporaryFolder tmp = new TemporaryFolder(); + private TempDir tempDir; + + @BeforeEach + void beforeEach() { + tempDir = new TempDir(); + } + + @AfterEach + void afterEach() { + tempDir.cleanup(); + } @Test - public void byteUnit() throws IOException { - final File path = tmp.newFile(); - try (Env env = create() - .setMaxReaders(1) - .setMapSize(MEBIBYTES.toBytes(1)) - .open(path, MDB_NOSUBDIR)) { + void byteUnit() { + 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, is(MEBIBYTES.toBytes(1))); + assertThat(info.mapSize).isEqualTo(ByteUnit.MEBIBYTES.toBytes(1)); } } - @Test(expected = AlreadyOpenException.class) - public void cannotChangeMapSizeAfterOpen() throws IOException { - final File path = tmp.newFile(); - final Builder builder = create() - .setMaxReaders(1); - try (Env env = builder.open(path, MDB_NOSUBDIR)) { - builder.setMapSize(1); - } + @Test + void cannotChangeMapSizeAfterOpen() { + assertThatThrownBy( + () -> { + 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(expected = AlreadyOpenException.class) - public void cannotChangeMaxDbsAfterOpen() throws IOException { - final File path = tmp.newFile(); - final Builder builder = create() - .setMaxReaders(1); - try (Env env = builder.open(path, MDB_NOSUBDIR)) { - builder.setMaxDbs(1); - } + @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); } - @Test(expected = AlreadyOpenException.class) - public void cannotChangeMaxReadersAfterOpen() throws IOException { - final File path = tmp.newFile(); - final Builder builder = create() - .setMaxReaders(1); - try (Env env = builder.open(path, MDB_NOSUBDIR)) { - builder.setMaxReaders(1); - } + @Test + void cannotChangeMaxDbsAfterOpen() { + assertThatThrownBy( + () -> { + 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); } - @Test(expected = AlreadyClosedException.class) - public void cannotInfoOnceClosed() throws IOException { - final File path = tmp.newFile(); - final Env env = create() - .setMaxReaders(1) - .open(path, MDB_NOSUBDIR); - env.close(); - env.info(); + @Test + void cannotChangeMaxReadersAfterOpen() { + assertThatThrownBy( + () -> { + 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); } - @Test(expected = AlreadyOpenException.class) - public void cannotOpenTwice() throws IOException { - final File path = tmp.newFile(); - final Builder builder = create() - .setMaxReaders(1); + @Test + void cannotInfoOnceClosed() { + assertThatThrownBy( + () -> { + final Path file = tempDir.createTempFile(); + final Env env = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); + env.close(); + env.info(); + }) + .isInstanceOf(AlreadyClosedException.class); + } - builder.open(path, MDB_NOSUBDIR).close(); - builder.open(path, MDB_NOSUBDIR); + @Test + void cannotOpenTwice() { + assertThatThrownBy( + () -> { + 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); } - @Test(expected = IllegalArgumentException.class) - public void cannotOverflowMapSize() { - final Builder builder = create() - .setMaxReaders(1); - final int mb = 1_024 * 1_024; - final int size = mb * 2_048; // as per issue 18 - builder.setMapSize(size); + @Test + void cannotOverflowMapSize() { + assertThatThrownBy( + () -> { + 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(expected = AlreadyClosedException.class) - public void cannotStatOnceClosed() throws IOException { - final File path = tmp.newFile(); - final Env env = create() - .setMaxReaders(1) - .open(path, MDB_NOSUBDIR); - env.close(); - env.stat(); + @Test + void negativeMapSize() { + assertThatThrownBy( + () -> { + final Builder builder = Env.create().setMaxReaders(1); + builder.setMapSize(-1); + }) + .isInstanceOf(IllegalArgumentException.class); } - @Test(expected = AlreadyClosedException.class) - public void cannotSyncOnceClosed() throws IOException { - final File path = tmp.newFile(); - final Env env = create() - .setMaxReaders(1) - .open(path, MDB_NOSUBDIR); - env.close(); - env.sync(false); + @Test + void negativeMapSize2() { + assertThatThrownBy( + () -> { + final Builder builder = Env.create().setMaxReaders(1); + builder.setMapSize(-1, ByteUnit.MEBIBYTES); + }) + .isInstanceOf(IllegalArgumentException.class); } @Test - public void copyDirectoryBased() throws IOException { - final File dest = tmp.newFolder(); - assertThat(dest.exists(), is(true)); - assertThat(dest.isDirectory(), is(true)); - assertThat(dest.list().length, is(0)); - final File src = tmp.newFolder(); - try (Env env = create() - .setMaxReaders(1) - .open(src)) { - env.copy(dest, MDB_CP_COMPACT); - assertThat(dest.list().length, is(1)); - } + void cannotStatOnceClosed() { + assertThatThrownBy( + () -> { + final Path file = tempDir.createTempFile(); + final Env env = + Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file); + env.close(); + env.stat(); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = InvalidCopyDestination.class) - public void copyDirectoryRejectsFileDestination() throws IOException { - final File dest = tmp.newFile(); - final File src = tmp.newFolder(); - try (Env env = create() - .setMaxReaders(1) - .open(src)) { - env.copy(dest, MDB_CP_COMPACT); - } + @Test + void cannotSyncOnceClosed() { + assertThatThrownBy( + () -> { + 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(expected = InvalidCopyDestination.class) - public void copyDirectoryRejectsMissingDestination() throws IOException { - final File dest = tmp.newFolder(); - assertThat(dest.delete(), is(true)); - final File src = tmp.newFolder(); - try (Env env = create() - .setMaxReaders(1) - .open(src)) { + @Test + void copyDirectoryBased() { + 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(expected = InvalidCopyDestination.class) - public void copyDirectoryRejectsNonEmptyDestination() throws IOException { - final File dest = tmp.newFolder(); - final File subDir = new File(dest, "hello"); - assertThat(subDir.mkdir(), is(true)); - final File src = tmp.newFolder(); - try (Env env = create() - .setMaxReaders(1) - .open(src)) { - env.copy(dest, MDB_CP_COMPACT); + @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 - public void copyFileBased() throws IOException { - final File dest = tmp.newFile(); - assertThat(dest.delete(), is(true)); - assertThat(dest.exists(), is(false)); - final File src = tmp.newFile(); - try (Env env = create() - .setMaxReaders(1) - .open(src, MDB_NOSUBDIR)) { - env.copy(dest, MDB_CP_COMPACT); - } - assertThat(dest.length(), greaterThan(0L)); + void copyDirectoryRejectsFileDestination() { + assertThatThrownBy( + () -> { + 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); + } + + @Test + void copyDirectoryRejectsMissingDestination() { + assertThatThrownBy( + () -> { + 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); + } + + @Test + void copyDirectoryRejectsNonEmptyDestination() { + assertThatThrownBy( + () -> { + 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(expected = InvalidCopyDestination.class) - public void copyFileRejectsExistingDestination() throws IOException { - final File dest = tmp.newFile(); - assertThat(dest.exists(), is(true)); - final File src = tmp.newFile(); - try (Env env = create() - .setMaxReaders(1) - .open(src, MDB_NOSUBDIR)) { + @Test + void copyFileBased() { + 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( + () -> { + 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 - public void createAsDirectory() throws IOException { - final File path = tmp.newFolder(); - final Env env = create() - .setMaxReaders(1) - .open(path); - assertThat(path.isDirectory(), is(true)); + void createAsDirectory() { + 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(), is(true)); + assertThat(env.isClosed()).isTrue(); env.close(); // safe to repeat } @Test - public void createAsFile() throws IOException { - final File path = tmp.newFile(); - try (Env env = create() - .setMapSize(1_024 * 1_024) - .setMaxDbs(1) - .setMaxReaders(1) - .open(path, MDB_NOSUBDIR)) { + void createAsFile() { + 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(path.isFile(), is(true)); + assertThat(Files.isRegularFile(file)).isTrue(); } } - @Test(expected = BadReaderLockException.class) - public void detectTransactionThreadViolation() throws IOException { - final File path = tmp.newFile(); - try (Env env = create() - .setMaxReaders(1) - .open(path, MDB_NOSUBDIR)) { - env.txnRead(); - env.txnRead(); - } + @Test + void detectTransactionThreadViolation() { + assertThatThrownBy( + () -> { + 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 - public void info() throws IOException { - final File path = tmp.newFile(); - try (Env env = create() - .setMaxReaders(4) - .setMapSize(123_456) - .open(path, MDB_NOSUBDIR)) { + void info() { + 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, is(notNullValue())); - assertThat(info.lastPageNumber, is(1L)); - assertThat(info.lastTransactionId, is(0L)); - assertThat(info.mapAddress, is(0L)); - assertThat(info.mapSize, is(123_456L)); - assertThat(info.maxReaders, is(4)); - assertThat(info.numReaders, is(0)); - assertThat(info.toString(), containsString("maxReaders=")); - assertThat(env.getMaxKeySize(), is(511)); + 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(expected = MapFullException.class) - @SuppressFBWarnings("DMI_RANDOM_USED_ONLY_ONCE") - public void mapFull() throws IOException { - final File path = tmp.newFolder(); - 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(path)) { - 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); - } - } + @Test + void mapFull() { + assertThatThrownBy( + () -> { + 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 - public void readOnlySupported() throws IOException { - final File path = tmp.newFolder(); - try (Env rwEnv = create() - .setMaxReaders(1) - .open(path)) { - final Dbi rwDb = rwEnv.openDbi(DB_1, MDB_CREATE); + void readOnlySupported() { + 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 = create() - .setMaxReaders(1) - .open(path, MDB_RDONLY_ENV)) { - final Dbi roDb = roEnv.openDbi(DB_1); + 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)), notNullValue()); + assertThat(roDb.get(roTxn, bb(1))).isNotNull(); } } } @Test - @SuppressFBWarnings("DMI_RANDOM_USED_ONLY_ONCE") - public void setMapSize() throws IOException { - final File path = tmp.newFolder(); + 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 = create() - .setMaxReaders(1) - .setMapSize(KIBIBYTES.toBytes(256)) - .setMaxDbs(1) - .open(path)) { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + 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; @@ -357,12 +471,26 @@ public void setMapSize() throws IOException { } catch (final MapFullException mfE) { mapFullExThrown = true; } - assertThat(mapFullExThrown, is(true)); + assertThat(mapFullExThrown).isTrue(); - env.setMapSize(KIBIBYTES.toBytes(512)); + 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()) { - assertThat(db.get(roTxn, bb(1)).getInt(), is(42)); + final ByteBuffer byteBuffer = db.get(roTxn, bb(1)); + assertThat(byteBuffer).isNotNull(); + assertThat(byteBuffer.getInt()).isEqualTo(42); } mapFullExThrown = false; @@ -377,37 +505,237 @@ public void setMapSize() throws IOException { } catch (final MapFullException mfE) { mapFullExThrown = true; } - assertThat(mapFullExThrown, is(false)); + assertThat(mapFullExThrown).isFalse(); } } @Test - public void stats() throws IOException { - final File path = tmp.newFile(); - try (Env env = create() - .setMaxReaders(1) - .open(path, MDB_NOSUBDIR)) { + void stats() { + final Path file = tempDir.createTempFile(); + try (Env env = Env.create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR).open(file)) { final Stat stat = env.stat(); - assertThat(stat, is(notNullValue())); - assertThat(stat.branchPages, is(0L)); - assertThat(stat.depth, is(0)); - assertThat(stat.entries, is(0L)); - assertThat(stat.leafPages, is(0L)); - assertThat(stat.overflowPages, is(0L)); - assertThat(stat.pageSize % 4_096, is(0)); - assertThat(stat.toString(), containsString("pageSize=")); + 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 - public void testDefaultOpen() throws IOException { - final File path = tmp.newFolder(); - try (Env env = open(path, 10)) { + void testDefaultOpen() { + final Path dir = tempDir.createTempDir(); + try (Env env = Env.create().setMapSize(10, ByteUnit.MEBIBYTES).open(dir)) { final EnvInfo info = env.info(); - assertThat(info.maxReaders, is(MAX_READERS_DEFAULT)); - final Dbi db = env.openDbi("test", MDB_CREATE); + 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 49d56b1d..4aa1245f 100644 --- a/src/test/java/org/lmdbjava/GarbageCollectionTest.java +++ b/src/test/java/org/lmdbjava/GarbageCollectionTest.java @@ -1,117 +1,106 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ package org.lmdbjava; import static java.nio.ByteBuffer.allocateDirect; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.junit.Assert.fail; +import static org.assertj.core.api.Assertions.fail; import static org.lmdbjava.DbiFlags.MDB_CREATE; -import static org.lmdbjava.Env.create; -import java.io.File; -import java.io.IOException; import java.nio.ByteBuffer; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; import org.mockito.Mockito; -@SuppressFBWarnings({"DM_GC", "RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT"}) -@SuppressWarnings("PMD.DoNotCallGarbageCollectionExplicitly") -public class GarbageCollectionTest { +class GarbageCollectionTest { private static final String DB_NAME = "my DB"; private static final String KEY_PREFIX = "Uncorruptedkey"; private static final String VAL_PREFIX = "Uncorruptedval"; - @Rule - public final TemporaryFolder tmp = new TemporaryFolder(); - @Test - public void buffersNotGarbageCollectedTest() throws IOException { - final File path = tmp.newFolder(); - try (Env env = create() - .setMapSize(2_085_760_999) - .setMaxDbs(1) - .open(path)) { - final Dbi db = env.openDbi(DB_NAME, MDB_CREATE); - - try (Txn txn = env.txnWrite()) { - for (int i = 0; i < 5_000; i++) { - putBuffer(db, txn, i); - } - txn.commit(); - } + void buffersNotGarbageCollectedTest() { + 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(); - // 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++) { + for (int i = 0; i < 5_000; 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()); + // 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()); + } } } } } } - private void putBuffer(final Dbi db, final Txn txn, - final int i) { + private void putBuffer(final Dbi db, final Txn txn, final int i) { final ByteBuffer key = allocateDirect(24); final ByteBuffer val = allocateDirect(24); key.put((KEY_PREFIX + i).getBytes(UTF_8)).flip(); val.put((VAL_PREFIX + i).getBytes(UTF_8)).flip(); db.put(txn, key, val); } - } diff --git a/src/test/java/org/lmdbjava/KeyRangeTest.java b/src/test/java/org/lmdbjava/KeyRangeTest.java index 3ea8c146..2e8854b9 100644 --- a/src/test/java/org/lmdbjava/KeyRangeTest.java +++ b/src/test/java/org/lmdbjava/KeyRangeTest.java @@ -1,28 +1,22 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ package org.lmdbjava; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.lmdbjava.KeyRange.all; import static org.lmdbjava.KeyRange.allBackward; import static org.lmdbjava.KeyRange.atLeast; @@ -45,153 +39,150 @@ import java.util.ArrayList; import java.util.List; - -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.lmdbjava.KeyRangeType.CursorOp; import org.lmdbjava.KeyRangeType.IteratorOp; /** * Test {@link KeyRange}. * - *

- * This test case focuses on the contractual correctness detailed in - * {@link KeyRangeType}. It does this using integers as per the JavaDoc - * examples. + *

This test case focuses on the contractual correctness detailed in {@link KeyRangeType}. It + * does this using integers as per the JavaDoc examples. */ public final class KeyRangeTest { private final FakeCursor cursor = new FakeCursor(); + @BeforeEach + void beforeEach() { + cursor.reset(); + } + @Test - public void allBackwardTest() { + void allBackwardTest() { verify(allBackward(), 8, 6, 4, 2); } @Test - public void allTest() { + void allTest() { verify(all(), 2, 4, 6, 8); } @Test - public void atLeastBackwardTest() { + void atLeastBackwardTest() { verify(atLeastBackward(5), 4, 2); verify(atLeastBackward(6), 6, 4, 2); verify(atLeastBackward(9), 8, 6, 4, 2); } @Test - public void atLeastTest() { + void atLeastTest() { verify(atLeast(5), 6, 8); verify(atLeast(6), 6, 8); } @Test - public void atMostBackwardTest() { + void atMostBackwardTest() { verify(atMostBackward(5), 8, 6); verify(atMostBackward(6), 8, 6); } @Test - public void atMostTest() { + void atMostTest() { verify(atMost(5), 2, 4); verify(atMost(6), 2, 4, 6); } - @Before - public void before() { - cursor.reset(); - } - @Test - public void closedBackwardTest() { + void closedBackwardTest() { verify(closedBackward(7, 3), 6, 4); verify(closedBackward(6, 2), 6, 4, 2); verify(closedBackward(9, 3), 8, 6, 4); } @Test - public void closedOpenBackwardTest() { + void closedOpenBackwardTest() { verify(closedOpenBackward(8, 3), 8, 6, 4); verify(closedOpenBackward(7, 2), 6, 4); verify(closedOpenBackward(9, 3), 8, 6, 4); } @Test - public void closedOpenTest() { + void closedOpenTest() { verify(closedOpen(3, 8), 4, 6); verify(closedOpen(2, 6), 2, 4); } @Test - public void closedTest() { + void closedTest() { verify(closed(3, 7), 4, 6); verify(closed(2, 6), 2, 4, 6); } @Test - public void fakeCursor() { - assertThat(cursor.first(), is(2)); - assertThat(cursor.next(), is(4)); - assertThat(cursor.next(), is(6)); - assertThat(cursor.next(), is(8)); - assertThat(cursor.next(), nullValue()); - assertThat(cursor.first(), is(2)); - assertThat(cursor.prev(), nullValue()); - assertThat(cursor.getWithSetRange(3), is(4)); - assertThat(cursor.next(), is(6)); - assertThat(cursor.getWithSetRange(1), is(2)); - assertThat(cursor.last(), is(8)); - assertThat(cursor.getWithSetRange(100), nullValue()); + void fakeCursor() { + assertThat(cursor.first()).isEqualTo(2); + assertThat(cursor.next()).isEqualTo(4); + assertThat(cursor.next()).isEqualTo(6); + assertThat(cursor.next()).isEqualTo(8); + assertThat(cursor.next()).isNull(); + assertThat(cursor.first()).isEqualTo(2); + assertThat(cursor.prev()).isNull(); + assertThat(cursor.getWithSetRange(3)).isEqualTo(4); + assertThat(cursor.next()).isEqualTo(6); + assertThat(cursor.getWithSetRange(1)).isEqualTo(2); + assertThat(cursor.last()).isEqualTo(8); + assertThat(cursor.getWithSetRange(100)).isNull(); } @Test - public void greaterThanBackwardTest() { + void greaterThanBackwardTest() { verify(greaterThanBackward(6), 4, 2); verify(greaterThanBackward(7), 6, 4, 2); verify(greaterThanBackward(9), 8, 6, 4, 2); } @Test - public void greaterThanTest() { + void greaterThanTest() { verify(greaterThan(4), 6, 8); verify(greaterThan(3), 4, 6, 8); } @Test - public void lessThanBackwardTest() { + void lessThanBackwardTest() { verify(lessThanBackward(5), 8, 6); verify(lessThanBackward(2), 8, 6, 4); } @Test - public void lessThanTest() { + void lessThanTest() { verify(lessThan(5), 2, 4); verify(lessThan(8), 2, 4, 6); } @Test - public void openBackwardTest() { + void openBackwardTest() { verify(openBackward(7, 2), 6, 4); verify(openBackward(8, 1), 6, 4, 2); verify(openBackward(9, 4), 8, 6); } @Test - public void openClosedBackwardTest() { + void openClosedBackwardTest() { verify(openClosedBackward(7, 2), 6, 4, 2); verify(openClosedBackward(8, 4), 6, 4); verify(openClosedBackward(9, 4), 8, 6, 4); } @Test - public void openClosedTest() { + void openClosedTest() { verify(openClosed(3, 8), 4, 6, 8); verify(openClosed(2, 6), 4, 6); } @Test - public void openTest() { + void openTest() { verify(open(3, 7), 4, 6); verify(open(2, 8), 4, 6); } @@ -203,8 +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()); @@ -221,21 +214,20 @@ private void verify(final KeyRange range, final int... expected) { } while (op != TERMINATE); for (int idx = 0; idx < results.size(); idx++) { - assertThat("idx " + idx, results.get(idx), is(expected[idx])); + assertThat(results.get(idx)).withFailMessage("idx " + idx).isEqualTo(expected[idx]); } - assertThat(results.size(), is(expected.length)); + assertThat(results.size()).isEqualTo(expected.length); } /** * Cursor that behaves like an LMDB cursor would. * - *

- * We use Integer rather than the primitive to represent a - * null buffer. + *

We use Integer rather than the primitive to represent a null + * buffer. */ private static final class FakeCursor { - private static final int[] KEYS = new int[]{2, 4, 6, 8}; + private static final int[] KEYS = new int[] {2, 4, 6, 8}; private int position; Integer apply(final CursorOp op, final Integer startKey) { @@ -302,7 +294,5 @@ Integer prev() { void reset() { position = 0; } - } - } diff --git a/src/test/java/org/lmdbjava/LibraryTest.java b/src/test/java/org/lmdbjava/LibraryTest.java index df5ae89d..3b492d75 100644 --- a/src/test/java/org/lmdbjava/LibraryTest.java +++ b/src/test/java/org/lmdbjava/LibraryTest.java @@ -1,49 +1,42 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ package org.lmdbjava; import static java.lang.Long.BYTES; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.lmdbjava.Library.RUNTIME; import static org.lmdbjava.TestUtils.invokePrivateConstructor; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.lmdbjava.Library.MDB_envinfo; -/** - * Test {@link Library}. - */ +/** Test {@link Library}. */ public final class LibraryTest { @Test - public void coverPrivateConstructors() { + void coverPrivateConstructors() { invokePrivateConstructor(Library.class); invokePrivateConstructor(UnsafeAccess.class); } @Test - public void structureFieldOrder() { + void structureFieldOrder() { final MDB_envinfo v = new MDB_envinfo(RUNTIME); - assertThat(v.f0_me_mapaddr.offset(), is(0L)); - assertThat(v.f1_me_mapsize.offset(), is((long) BYTES)); + assertThat(v.f0_me_mapaddr.offset()).isEqualTo(0L); + assertThat(v.f1_me_mapsize.offset()).isEqualTo(BYTES); } } diff --git a/src/test/java/org/lmdbjava/MaskedFlagTest.java b/src/test/java/org/lmdbjava/MaskedFlagTest.java index 0dbc8b62..25c23797 100644 --- a/src/test/java/org/lmdbjava/MaskedFlagTest.java +++ b/src/test/java/org/lmdbjava/MaskedFlagTest.java @@ -1,76 +1,68 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ package org.lmdbjava; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.arrayWithSize; +import static org.assertj.core.api.Assertions.assertThat; import static org.lmdbjava.EnvFlags.MDB_FIXEDMAP; import static org.lmdbjava.EnvFlags.MDB_NOSYNC; import static org.lmdbjava.EnvFlags.MDB_RDONLY_ENV; import static org.lmdbjava.MaskedFlag.isSet; import static org.lmdbjava.MaskedFlag.mask; -import org.junit.Test; +import org.junit.jupiter.api.Test; -/** - * Test {@link MaskedFlag}. - */ +/** Test {@link MaskedFlag}. */ public final class MaskedFlagTest { @Test - public void isSetOperates() { - assertThat(isSet(0, MDB_NOSYNC), is(false)); - assertThat(isSet(0, MDB_FIXEDMAP), is(false)); - assertThat(isSet(0, MDB_RDONLY_ENV), is(false)); + void isSetOperates() { + assertThat(isSet(0, MDB_NOSYNC)).isFalse(); + assertThat(isSet(0, MDB_FIXEDMAP)).isFalse(); + assertThat(isSet(0, MDB_RDONLY_ENV)).isFalse(); - assertThat(isSet(MDB_FIXEDMAP.getMask(), MDB_NOSYNC), is(false)); - assertThat(isSet(MDB_FIXEDMAP.getMask(), MDB_FIXEDMAP), is(true)); - assertThat(isSet(MDB_FIXEDMAP.getMask(), MDB_RDONLY_ENV), is(false)); + assertThat(isSet(MDB_FIXEDMAP.getMask(), MDB_NOSYNC)).isFalse(); + assertThat(isSet(MDB_FIXEDMAP.getMask(), MDB_FIXEDMAP)).isTrue(); + assertThat(isSet(MDB_FIXEDMAP.getMask(), MDB_RDONLY_ENV)).isFalse(); - assertThat(isSet(MDB_NOSYNC.getMask(), MDB_NOSYNC), is(true)); - assertThat(isSet(MDB_NOSYNC.getMask(), MDB_FIXEDMAP), is(false)); - assertThat(isSet(MDB_NOSYNC.getMask(), MDB_RDONLY_ENV), is(false)); + assertThat(isSet(MDB_NOSYNC.getMask(), MDB_NOSYNC)).isTrue(); + assertThat(isSet(MDB_NOSYNC.getMask(), MDB_FIXEDMAP)).isFalse(); + assertThat(isSet(MDB_NOSYNC.getMask(), MDB_RDONLY_ENV)).isFalse(); final int syncFixed = mask(MDB_NOSYNC, MDB_FIXEDMAP); - assertThat(isSet(syncFixed, MDB_NOSYNC), is(true)); - assertThat(isSet(syncFixed, MDB_FIXEDMAP), is(true)); - assertThat(isSet(syncFixed, MDB_RDONLY_ENV), is(false)); + assertThat(isSet(syncFixed, MDB_NOSYNC)).isTrue(); + assertThat(isSet(syncFixed, MDB_FIXEDMAP)).isTrue(); + assertThat(isSet(syncFixed, MDB_RDONLY_ENV)).isFalse(); } @Test - public void masking() { + void masking() { final EnvFlags[] nullFlags = null; - assertThat(mask(nullFlags), is(0)); + assertThat(mask(nullFlags)).isEqualTo(0); - final EnvFlags[] emptyFlags = new EnvFlags[]{}; - assertThat(mask(emptyFlags), is(0)); + final EnvFlags[] emptyFlags = new EnvFlags[] {}; + assertThat(mask(emptyFlags)).isEqualTo(0); - final EnvFlags[] nullElementZero = new EnvFlags[]{null}; - assertThat(nullElementZero, is(arrayWithSize(1))); - assertThat(mask(nullElementZero), is(0)); + final EnvFlags[] nullElementZero = new EnvFlags[] {null}; + assertThat(nullElementZero.length).isEqualTo(1); + assertThat(mask(nullElementZero)).isEqualTo(0); - assertThat(mask(MDB_NOSYNC), is(MDB_NOSYNC.getMask())); + assertThat(mask(MDB_NOSYNC)).isEqualTo(MDB_NOSYNC.getMask()); final int expected = MDB_NOSYNC.getMask() + MDB_FIXEDMAP.getMask(); - assertThat(mask(MDB_NOSYNC, MDB_FIXEDMAP), is(expected)); + assertThat(mask(MDB_NOSYNC, MDB_FIXEDMAP)).isEqualTo(expected); } } diff --git a/src/test/java/org/lmdbjava/MetaTest.java b/src/test/java/org/lmdbjava/MetaTest.java index 922c4368..3e893561 100644 --- a/src/test/java/org/lmdbjava/MetaTest.java +++ b/src/test/java/org/lmdbjava/MetaTest.java @@ -1,39 +1,30 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ package org.lmdbjava; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.lmdbjava.LmdbNativeException.PageCorruptedException.MDB_CORRUPTED; import static org.lmdbjava.Meta.error; import static org.lmdbjava.TestUtils.invokePrivateConstructor; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.lmdbjava.Meta.Version; -/** - * Test {@link Meta}. - */ +/** Test {@link Meta}. */ public final class MetaTest { @Test @@ -42,17 +33,15 @@ public void coverPrivateConstructors() { } @Test - public void errCode() { - assertThat(error(MDB_CORRUPTED), is( - "MDB_CORRUPTED: Located page was wrong type")); + void errCode() { + assertThat(error(MDB_CORRUPTED)).isEqualTo("MDB_CORRUPTED: Located page was wrong type"); } @Test - public void version() { + void version() { final Version v = Meta.version(); - assertThat(v, not(nullValue())); - assertThat(v.major, is(0)); - assertThat(v.minor, is(9)); + assertThat(v).isNotNull(); + assertThat(v.major).isEqualTo(0); + assertThat(v.minor).isEqualTo(9); } - } 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/ResultCodeMapperTest.java b/src/test/java/org/lmdbjava/ResultCodeMapperTest.java index 723c22ca..c363d60a 100644 --- a/src/test/java/org/lmdbjava/ResultCodeMapperTest.java +++ b/src/test/java/org/lmdbjava/ResultCodeMapperTest.java @@ -1,40 +1,32 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ package org.lmdbjava; import static java.lang.Integer.MAX_VALUE; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.hasSize; -import static org.junit.Assert.fail; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.fail; import static org.lmdbjava.Cursor.FullException.MDB_CURSOR_FULL; import static org.lmdbjava.ResultCodeMapper.checkRc; import static org.lmdbjava.TestUtils.invokePrivateConstructor; import java.util.HashSet; import java.util.Set; - -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.lmdbjava.Cursor.FullException; import org.lmdbjava.Dbi.BadDbiException; import org.lmdbjava.Dbi.BadValueSizeException; @@ -57,9 +49,7 @@ import org.lmdbjava.Txn.BadReaderLockException; import org.lmdbjava.Txn.TxFullException; -/** - * Test {@link ResultCodeMapper} and {@link LmdbException}. - */ +/** Test {@link ResultCodeMapper} and {@link LmdbException}. */ public final class ResultCodeMapperTest { private static final Set EXCEPTIONS = new HashSet<>(); @@ -94,72 +84,71 @@ public final class ResultCodeMapperTest { } @Test - public void checkErrAll() { + void checkErrAll() { for (final Integer rc : RESULT_CODES) { try { checkRc(rc); fail("Exception expected for RC " + rc); } catch (final LmdbNativeException e) { - assertThat(e.getResultCode(), is(rc)); + assertThat(e.getResultCode()).isEqualTo(rc); } } } - @Test(expected = ConstantDerivedException.class) - public void checkErrConstantDerived() { - checkRc(20); + @Test + void checkErrConstantDerived() { + assertThatThrownBy(() -> checkRc(20)).isInstanceOf(ConstantDerivedException.class); } @Test - public void checkErrConstantDerivedMessage() { + void checkErrConstantDerivedMessage() { try { checkRc(2); fail("Should have raised exception"); } catch (final ConstantDerivedException ex) { - assertThat(ex.getMessage(), containsString("No such file or directory")); + assertThat(ex.getMessage()).contains("No such file or directory"); } } - @Test(expected = FullException.class) - public void checkErrCursorFull() { - checkRc(MDB_CURSOR_FULL); + @Test + void checkErrCursorFull() { + assertThatThrownBy(() -> checkRc(MDB_CURSOR_FULL)).isInstanceOf(FullException.class); } - @Test(expected = IllegalArgumentException.class) - public void checkErrUnknownResultCode() { - checkRc(MAX_VALUE); + @Test + void checkErrUnknownResultCode() { + assertThatThrownBy(() -> checkRc(MAX_VALUE)).isInstanceOf(IllegalArgumentException.class); } @Test - public void coverPrivateConstructors() { + void coverPrivateConstructors() { invokePrivateConstructor(ResultCodeMapper.class); } @Test - public void lmdbExceptionPreservesRootCause() { + void lmdbExceptionPreservesRootCause() { final Exception cause = new IllegalStateException("root cause"); final LmdbException e = new LmdbException("test", cause); - assertThat(e.getCause(), is(cause)); - assertThat(e.getMessage(), is("test")); + assertThat(e.getCause()).isEqualTo(cause); + assertThat(e.getMessage()).isEqualTo("test"); } @Test - public void mapperReturnsUnique() { + void mapperReturnsUnique() { final Set seen = new HashSet<>(); for (final Integer rc : RESULT_CODES) { try { checkRc(rc); } catch (final LmdbNativeException ex) { - assertThat(ex, is(notNullValue())); + assertThat(ex).isNotNull(); seen.add(ex); } } - assertThat(seen, hasSize(RESULT_CODES.size())); + assertThat(seen).hasSize(RESULT_CODES.size()); } @Test - public void noDuplicateResultCodes() { - assertThat(RESULT_CODES.size(), is(EXCEPTIONS.size())); + void noDuplicateResultCodes() { + assertThat(RESULT_CODES.size()).isEqualTo(EXCEPTIONS.size()); } - } diff --git a/src/test/java/org/lmdbjava/TargetNameTest.java b/src/test/java/org/lmdbjava/TargetNameTest.java index eec38233..6c0a8f44 100644 --- a/src/test/java/org/lmdbjava/TargetNameTest.java +++ b/src/test/java/org/lmdbjava/TargetNameTest.java @@ -1,74 +1,73 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ package org.lmdbjava; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.lmdbjava.TargetName.isExternal; import static org.lmdbjava.TargetName.resolveFilename; import static org.lmdbjava.TestUtils.invokePrivateConstructor; -import org.junit.Test; +import org.junit.jupiter.api.Test; -/** - * Test {@link TargetName}. - */ +/** Test {@link TargetName}. */ public final class TargetNameTest { private static final String NONE = ""; @Test - public void coverPrivateConstructors() { + void coverPrivateConstructors() { invokePrivateConstructor(TargetName.class); } @Test - public void customEmbedded() { - assertThat(resolveFilename(NONE, "x/y.so", NONE, NONE), is("x/y.so")); - assertThat(isExternal(NONE), is(false)); + void customEmbedded() { + assertThat(resolveFilename(NONE, "x/y.so", NONE, NONE)).isEqualTo("x/y.so"); + assertThat(isExternal(NONE)).isFalse(); } @Test - public void embeddedNameResolution() { - embed("aarch64-linux-gnu.so", "aarch64", "Linux"); + void embeddedNameResolution() { + // 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"); } @Test - public void externalLibrary() { - assertThat(resolveFilename("/l.so", NONE, NONE, NONE), is("/l.so")); - assertThat(TargetName.isExternal("/l.so"), is(true)); + void externalLibrary() { + assertThat(resolveFilename("/l.so", NONE, NONE, NONE)).isEqualTo("/l.so"); + assertThat(TargetName.isExternal("/l.so")).isTrue(); } @Test - public void externalTakesPriority() { - assertThat(resolveFilename("/lm.so", "x/y.so", NONE, NONE), is("/lm.so")); - assertThat(isExternal("/lm.so"), is(true)); + void externalTakesPriority() { + assertThat(resolveFilename("/lm.so", "x/y.so", NONE, NONE)).isEqualTo("/lm.so"); + assertThat(isExternal("/lm.so")).isTrue(); } private void embed(final String lib, final String arch, final String os) { - assertThat(resolveFilename(NONE, NONE, arch, os), is("org/lmdbjava/" + lib)); - assertThat(isExternal(NONE), is(false)); + assertThat(resolveFilename(NONE, NONE, arch, os)).isEqualTo("org/lmdbjava/native/" + lib); + assertThat(isExternal(NONE)).isFalse(); } } 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 05dfb0cc..a15dc6b2 100644 --- a/src/test/java/org/lmdbjava/TestUtils.java +++ b/src/test/java/org/lmdbjava/TestUtils.java @@ -1,85 +1,205 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ - package org.lmdbjava; import static io.netty.buffer.PooledByteBufAllocator.DEFAULT; -import static java.lang.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 io.netty.buffer.ByteBuf; +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; -/** - * Static constants and methods that are convenient when writing LMDB-related - * tests. - */ +/** Static constants and methods that are convenient when writing LMDB-related tests. */ final class TestUtils { public static final String DB_1 = "test-db-1"; + public static final String DB_2 = "test-db-2"; + public static final String DB_3 = "test-db-3"; + public static final String DB_4 = "test-db-2"; - @SuppressWarnings("PMD.AvoidUsingOctalValues") - public static final int POSIX_MODE = 0664; + private TestUtils() {} - private TestUtils() { + static byte[] ba(final int value) { + byte[] bytes = new byte[4]; + ByteBuffer.wrap(bytes).putInt(value); + return bytes; } - static byte[] ba(final int value) { - final MutableDirectBuffer b = new UnsafeBuffer(new byte[4]); - b.putInt(0, value); - return b.byteArray(); + static int fromBa(final byte[] ba) { + return ByteBuffer.wrap(ba).getInt(); } 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(); c.setAccessible(true); c.newInstance(); - } catch (final NoSuchMethodException | InstantiationException - | IllegalAccessException | IllegalArgumentException - | InvocationTargetException e) { + } catch (final NoSuchMethodException + | InstantiationException + | IllegalAccessException + | IllegalArgumentException + | InvocationTargetException e) { throw new LmdbException("Private construction failed", e); } } 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 0385d952..c2271363 100644 --- a/src/test/java/org/lmdbjava/TutorialTest.java +++ b/src/test/java/org/lmdbjava/TutorialTest.java @@ -1,114 +1,109 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ package org.lmdbjava; -import static java.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.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.startsWith; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; +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.io.File; -import java.io.IOException; 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.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.lmdbjava.CursorIterable.KeyVal; /** * Welcome to LmdbJava! * - *

- * This short tutorial will walk you through using LmdbJava step-by-step. + *

This short tutorial will walk you through using LmdbJava step-by-step. * - *

- * If you are using a 64-bit Windows, Linux or OS X machine, you can simply run - * this tutorial by adding the LmdbJava JAR to your classpath. It includes the - * required system libraries. If you are using another 64-bit platform, you'll - * need to install the LMDB system library yourself. 32-bit platforms are not - * supported. + *

If you are using a 64-bit Windows, Linux or OS X machine, you can simply run this tutorial by + * adding the LmdbJava JAR to your classpath. It includes the required system libraries. If you are + * using another 64-bit platform, you'll need to install the LMDB system library yourself. 32-bit + * platforms are not supported. * - *

- * Start the JVM with arguments --add-opens java.base/java.nio=ALL-UNNAMED + *

Start the JVM with arguments --add-opens java.base/java.nio=ALL-UNNAMED * --add-opens java.base/sun.nio.ch=ALL-UNNAMED to suppress JVM warnings. */ public final class TutorialTest { private static final String DB_NAME = "my DB"; - @Rule - public final TemporaryFolder tmp = new TemporaryFolder(); - /** - * In this first tutorial we will use LmdbJava with some basic defaults. - * - * @throws IOException if a path was unavailable for memory mapping - */ + 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 - @SuppressWarnings("ConvertToTryWithResources") - public void tutorial1() throws IOException { + void tutorial1() { // We need a storage directory first. // The path cannot be on a remote file system. - final File path = tmp.newFolder(); + 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 = 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(path); + 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.openDbi(DB_NAME, MDB_CREATE); + 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 = allocateDirect(env.getMaxKeySize()); - final ByteBuffer val = allocateDirect(700); + 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(); @@ -125,14 +120,14 @@ public void tutorial1() throws IOException { // 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); - assertNotNull(found); + assertThat(found).isNotNull(); // The fetchedVal is read-only and points to LMDB memory final ByteBuffer fetchedVal = txn.val(); - assertThat(fetchedVal.remaining(), is(valSize)); + assertThat(fetchedVal.remaining()).isEqualTo(valSize); // Let's double-check the fetched value is correct - assertThat(UTF_8.decode(fetchedVal).toString(), is("Hello world")); + assertThat(UTF_8.decode(fetchedVal).toString()).isEqualTo("Hello world"); } // We can also delete. The simplest way is to let Dbi allocate a new Txn... @@ -140,99 +135,99 @@ public void tutorial1() throws IOException { // Now if we try to fetch the deleted row, it won't be present try (Txn txn = env.txnRead()) { - assertNull(db.get(txn, key)); + assertThat(db.get(txn, key)).isNull(); } env.close(); } - /** - * In this second tutorial we'll learn more about LMDB's ACID Txns. - * - * @throws IOException if a path was unavailable for memory mapping - * @throws InterruptedException if executor shutdown interrupted - */ + /** In this second tutorial we'll learn more about LMDB's ACID Txns. */ @Test - @SuppressWarnings("ConvertToTryWithResources") - public void tutorial2() throws IOException, InterruptedException { - final Env env = createSimpleEnv(tmp.newFolder()); - 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. - 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); - assertNotNull(found); - - // 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); - assertNotNull(found); - - // 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(() -> { + void tutorial2() { + 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.clear(); - key.put("key2".getBytes(UTF_8)).flip(); + 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(); } - }); - 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); - assertNull(found); - - // 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); - assertNotNull(found); - - // 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(); + + // 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.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); + } } /** - * In this third tutorial we'll have a look at the Cursor. Up until now we've - * just used Dbi, which is good enough for simple cases but unsuitable if you - * don't know the key to fetch, or want to iterate over all the data etc. - * - * @throws IOException if a path was unavailable for memory mapping + * In this third tutorial we'll have a look at the Cursor. Up until now we've just used Dbi, which + * is good enough for simple cases but unsuitable if you don't know the key to fetch, or want to + * iterate over all the data etc. */ @Test - @SuppressWarnings("ConvertToTryWithResources") - public void tutorial3() throws IOException { - final Env env = createSimpleEnv(tmp.newFolder()); - final Dbi db = env.openDbi(DB_NAME, MDB_CREATE); - final ByteBuffer key = allocateDirect(env.getMaxKeySize()); - final ByteBuffer val = allocateDirect(700); + void tutorial3() { + 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. @@ -252,17 +247,17 @@ public void tutorial3() throws IOException { // We can read from the Cursor by key. c.get(key, MDB_SET); - assertThat(UTF_8.decode(c.key()).toString(), is("ccc")); + 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(), is("aaa")); + assertThat(UTF_8.decode(c.key()).toString()).isEqualTo("aaa"); c.seek(MDB_LAST); - assertThat(UTF_8.decode(c.key()).toString(), is("zzz")); + assertThat(UTF_8.decode(c.key()).toString()).isEqualTo("zzz"); c.seek(MDB_PREV); - assertThat(UTF_8.decode(c.key()).toString(), is("ccc")); + assertThat(UTF_8.decode(c.key()).toString()).isEqualTo("ccc"); // Cursors can also delete the current key. c.delete(); @@ -296,20 +291,19 @@ public void tutorial3() throws IOException { } /** - * In this fourth tutorial we'll take a quick look at the iterators. These are - * a more Java idiomatic form of using the Cursors we looked at in tutorial 3. - * - * @throws IOException if a path was unavailable for memory mapping + * In this fourth tutorial we'll take a quick look at the iterators. These are a more Java + * idiomatic form of using the Cursors we looked at in tutorial 3. */ @Test - @SuppressWarnings("ConvertToTryWithResources") - public void tutorial4() throws IOException { - final Env env = createSimpleEnv(tmp.newFolder()); - final Dbi db = env.openDbi(DB_NAME, MDB_CREATE); + void tutorial4() { + 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 = allocateDirect(env.getMaxKeySize()); - final ByteBuffer val = allocateDirect(700); + 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. @@ -326,17 +320,16 @@ public void tutorial4() throws IOException { // 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(), notNullValue()); - assertThat(kv.val(), notNullValue()); + 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())) { + try (CursorIterable ci = db.iterate(txn, KeyRange.allBackward())) { for (final KeyVal kv : ci) { - assertThat(kv.key(), notNullValue()); - assertThat(kv.val(), notNullValue()); + assertThat(kv.key()).isNotNull(); + assertThat(kv.val()).isNotNull(); } } @@ -347,8 +340,8 @@ public void tutorial4() throws IOException { final KeyRange range = KeyRange.atLeastBackward(key); try (CursorIterable ci = db.iterate(txn, range)) { for (final KeyVal kv : ci) { - assertThat(kv.key(), notNullValue()); - assertThat(kv.val(), notNullValue()); + assertThat(kv.key()).isNotNull(); + assertThat(kv.val()).isNotNull(); } } } @@ -356,23 +349,24 @@ public void tutorial4() throws IOException { env.close(); } - /** - * In this fifth tutorial we'll explore multiple values sharing a single key. - * - * @throws IOException if a path was unavailable for memory mapping - */ + /** In this fifth tutorial we'll explore multiple values sharing a single key. */ @Test - @SuppressWarnings("ConvertToTryWithResources") - public void tutorial5() throws IOException { - final Env env = createSimpleEnv(tmp.newFolder()); + void tutorial5() { + 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.openDbi(DB_NAME, MDB_CREATE, MDB_DUPSORT); + 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 = allocateDirect(env.getMaxKeySize()); - final ByteBuffer val = allocateDirect(env.getMaxKeySize()); + 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); @@ -390,17 +384,17 @@ public void tutorial5() throws IOException { // Cursor can tell us how many values the current key has. final long count = c.count(); - assertThat(count, is(3L)); + assertThat(count).isEqualTo(3L); // Let's position the Cursor. Note sorting still works. c.seek(MDB_FIRST); - assertThat(UTF_8.decode(c.val()).toString(), is("kkk")); + assertThat(UTF_8.decode(c.val()).toString()).isEqualTo("kkk"); c.seek(MDB_LAST); - assertThat(UTF_8.decode(c.val()).toString(), is("xxx")); + assertThat(UTF_8.decode(c.val()).toString()).isEqualTo("xxx"); c.seek(MDB_PREV); - assertThat(UTF_8.decode(c.val()).toString(), is("lll")); + assertThat(UTF_8.decode(c.val()).toString()).isEqualTo("lll"); c.close(); txn.commit(); @@ -410,20 +404,18 @@ public void tutorial5() throws IOException { } /** - * Next up we'll show you how to easily check your platform (operating system - * and Java version) is working properly with LmdbJava and the embedded LMDB - * native library. - * - * @throws IOException if a path was unavailable for memory mapping + * Next up we'll show you how to easily check your platform (operating system and Java version) is + * working properly with LmdbJava and the embedded LMDB native library. */ @Test - @SuppressWarnings("ConvertToTryWithResources") - public void tutorial6() throws IOException { + void tutorial6() { + final Path dir = tempDir.createTempDir(); // 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(tmp.newFolder()); + 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); @@ -435,27 +427,22 @@ public void tutorial6() throws IOException { env.close(); } - /** - * In this final tutorial we'll look at using Agrona's DirectBuffer. - * - * @throws IOException if a path was unavailable for memory mapping - */ + /** In this final tutorial we'll look at using Agrona's DirectBuffer. */ @Test - @SuppressWarnings("ConvertToTryWithResources") - public void tutorial7() throws IOException { + void tutorial7() { + 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 = create(PROXY_DB) - .setMapSize(10_485_760) - .setMaxDbs(1) - .open(tmp.newFolder()); + final Env env = + Env.create(PROXY_DB).setMapSize(10, ByteUnit.MEBIBYTES).setMaxDbs(1).open(dir); - final Dbi db = env.openDbi(DB_NAME, MDB_CREATE); + final Dbi db = + env.createDbi().setDbName(DB_NAME).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); - final ByteBuffer keyBb = allocateDirect(env.getMaxKeySize()); + final ByteBuffer keyBb = ByteBuffer.allocateDirect(env.getMaxKeySize()); final MutableDirectBuffer key = new UnsafeBuffer(keyBb); - final MutableDirectBuffer val = new UnsafeBuffer(allocateDirect(700)); + final MutableDirectBuffer val = new UnsafeBuffer(ByteBuffer.allocateDirect(700)); try (Txn txn = env.txnWrite()) { try (Cursor c = db.openCursor(txn)) { @@ -468,31 +455,29 @@ public void tutorial7() throws IOException { c.put(key, val); c.seek(MDB_FIRST); - assertThat(c.key().getStringWithoutLengthUtf8(0, env.getMaxKeySize()), - startsWith("ggg")); + assertThat(c.key().getStringWithoutLengthUtf8(0, env.getMaxKeySize())).startsWith("ggg"); c.seek(MDB_LAST); - assertThat(c.key().getStringWithoutLengthUtf8(0, env.getMaxKeySize()), - startsWith("yyy")); + 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, is(12)); - assertThat(key.capacity(), is(env.getMaxKeySize())); + 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(), is(keyLen)); + assertThat(key.capacity()).isEqualTo(keyLen); c.put(key, val); c.seek(MDB_FIRST); - assertThat(c.key().capacity(), is(keyLen)); - assertThat(c.key().getStringWithoutLengthUtf8(0, c.key().capacity()), - is("12characters")); + 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(), is(env.getMaxKeySize())); + assertThat(key.capacity()).isEqualTo(env.getMaxKeySize()); } txn.commit(); } @@ -500,16 +485,137 @@ public void tutorial7() throws IOException { 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 // how to speed up inserts by appending them in key order, using integer // 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 File path) { - return create() - .setMapSize(10_485_760) - .setMaxDbs(1) - .setMaxReaders(1) - .open(path); + private Env createSimpleEnv(final Path path) { + 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 775a10a6..7210b613 100644 --- a/src/test/java/org/lmdbjava/TxnTest.java +++ b/src/test/java/org/lmdbjava/TxnTest.java @@ -1,41 +1,31 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ package org.lmdbjava; -import static com.jakewharton.byteunits.BinaryByteUnit.KIBIBYTES; import static java.nio.ByteBuffer.allocateDirect; import static java.nio.charset.StandardCharsets.UTF_8; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.CoreMatchers.nullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.lmdbjava.DbiFlags.MDB_CREATE; import static org.lmdbjava.Env.create; import static org.lmdbjava.EnvFlags.MDB_NOSUBDIR; 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; @@ -43,18 +33,14 @@ import static org.lmdbjava.Txn.State.RESET; import static org.lmdbjava.TxnFlags.MDB_RDONLY_TXN; -import java.io.File; -import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicLong; - -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.lmdbjava.Dbi.BadValueSizeException; import org.lmdbjava.Env.AlreadyClosedException; import org.lmdbjava.Txn.EnvIsReadOnly; @@ -65,42 +51,54 @@ import org.lmdbjava.Txn.ReadWriteRequiredException; import org.lmdbjava.Txn.ResetException; -/** - * Test {@link Txn}. - */ +/** Test {@link Txn}. */ public final class TxnTest { - @Rule - public final TemporaryFolder tmp = new TemporaryFolder(); + private Path file; private Env env; - private File path; - @After - public void after() { - env.close(); + 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); } - @Before - public void before() throws IOException { - path = tmp.newFile(); - env = create() - .setMapSize(KIBIBYTES.toBytes(256)) - .setMaxReaders(1) - .setMaxDbs(2) - .open(path, POSIX_MODE, MDB_NOSUBDIR); + @AfterEach + void afterEach() { + env.close(); + tempDir.cleanup(); } - @Test(expected = BadValueSizeException.class) - public void largeKeysRejected() throws IOException { - final Dbi dbi = env.openDbi(DB_1, MDB_CREATE); - final ByteBuffer key = allocateDirect(env.getMaxKeySize() + 1); - key.limit(key.capacity()); - dbi.put(key, bb(2)); + @Test + void largeKeysRejected() { + assertThatThrownBy( + () -> { + 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)); + }) + .isInstanceOf(BadValueSizeException.class); } @Test - public void rangeSearch() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); + void rangeSearch() { + 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(); @@ -128,58 +126,75 @@ public void rangeSearch() { } } - assertEquals(3, keysFound.size()); - + assertThat(keysFound.size()).isEqualTo(3); } } @Test - public void readOnlyTxnAllowedInReadOnlyEnv() { - env.openDbi(DB_1, MDB_CREATE); - try (Env roEnv = create() - .setMaxReaders(1) - .open(path, MDB_NOSUBDIR, MDB_RDONLY_ENV)) { - assertThat(roEnv.txnRead(), is(notNullValue())); + void readOnlyTxnAllowedInReadOnlyEnv() { + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); + try (Env roEnv = + create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { + assertThat(roEnv.txnRead()).isNotNull(); } } - @Test(expected = EnvIsReadOnly.class) - public void readWriteTxnDeniedInReadOnlyEnv() { - env.openDbi(DB_1, MDB_CREATE); - env.close(); - try (Env roEnv = create() - .setMaxReaders(1) - .open(path, MDB_NOSUBDIR, MDB_RDONLY_ENV)) { - roEnv.txnWrite(); // error - } + @Test + void readWriteTxnDeniedInReadOnlyEnv() { + assertThatThrownBy( + () -> { + env.createDbi() + .setDbName(DB_1) + .withDefaultComparator() + .setDbiFlags(MDB_CREATE) + .open(); + env.close(); + try (Env roEnv = + create().setMaxReaders(1).setEnvFlags(MDB_NOSUBDIR, MDB_RDONLY_ENV).open(file)) { + roEnv.txnWrite(); // error + } + }) + .isInstanceOf(EnvIsReadOnly.class); } - @Test(expected = NotReadyException.class) - public void testCheckNotCommitted() { - try (Txn txn = env.txnRead()) { - txn.commit(); - txn.checkReady(); - } + @Test + void testCheckNotCommitted() { + assertThatThrownBy( + () -> { + try (Txn txn = env.txnRead()) { + txn.commit(); + txn.checkReady(); + } + }) + .isInstanceOf(NotReadyException.class); } - @Test(expected = ReadOnlyRequiredException.class) - public void testCheckReadOnly() { - try (Txn txn = env.txnWrite()) { - txn.checkReadOnly(); - } + @Test + void testCheckReadOnly() { + assertThatThrownBy( + () -> { + try (Txn txn = env.txnWrite()) { + txn.checkReadOnly(); + } + }) + .isInstanceOf(ReadOnlyRequiredException.class); } - @Test(expected = ReadWriteRequiredException.class) - public void testCheckWritesAllowed() { - try (Txn txn = env.txnRead()) { - txn.checkWritesAllowed(); - } + @Test + void testCheckWritesAllowed() { + assertThatThrownBy( + () -> { + try (Txn txn = env.txnRead()) { + txn.checkWritesAllowed(); + } + }) + .isInstanceOf(ReadWriteRequiredException.class); } @Test - public void testGetId() { - final Dbi db = env.openDbi(DB_1, MDB_CREATE); - + void testGetId() { + final Dbi db = + env.createDbi().setDbName(DB_1).withDefaultComparator().setDbiFlags(MDB_CREATE).open(); final AtomicLong txId1 = new AtomicLong(); final AtomicLong txId2 = new AtomicLong(); @@ -193,175 +208,258 @@ public void testGetId() { txId2.set(tx2.getId()); } // should not see the same snapshot - assertThat(txId1.get(), is(not(txId2.get()))); + assertThat(txId1.get()).isNotEqualTo(txId2.get()); } @Test - public void txCanCommitThenCloseWithoutError() { + void txCanCommitThenCloseWithoutError() { try (Txn txn = env.txnRead()) { - assertThat(txn.getState(), is(READY)); + assertThat(txn.getState()).isEqualTo(READY); txn.commit(); - assertThat(txn.getState(), is(DONE)); + assertThat(txn.getState()).isEqualTo(DONE); } } - @Test(expected = NotReadyException.class) - public void txCannotAbortIfAlreadyCommitted() { - try (Txn txn = env.txnRead()) { - assertThat(txn.getState(), is(READY)); - txn.commit(); - assertThat(txn.getState(), is(DONE)); - txn.abort(); - } + @Test + void txCannotAbortIfAlreadyCommitted() { + assertThatThrownBy( + () -> { + try (Txn txn = env.txnRead()) { + assertThat(txn.getState()).isEqualTo(READY); + txn.commit(); + assertThat(txn.getState()).isEqualTo(DONE); + txn.abort(); + } + }) + .isInstanceOf(NotReadyException.class); } - @Test(expected = NotReadyException.class) - public void txCannotCommitTwice() { - try (Txn txn = env.txnRead()) { - txn.commit(); - txn.commit(); // error - } + @Test + void txCannotCommitTwice() { + assertThatThrownBy( + () -> { + try (Txn txn = env.txnRead()) { + txn.commit(); + txn.commit(); // error + } + }) + .isInstanceOf(NotReadyException.class); } - @Test(expected = AlreadyClosedException.class) - @SuppressWarnings("ResultOfObjectAllocationIgnored") - public void txConstructionDeniedIfEnvClosed() { - env.close(); - env.txnRead(); + @Test + void txConstructionDeniedIfEnvClosed() { + assertThatThrownBy( + () -> { + env.close(); + env.txnRead(); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = AlreadyClosedException.class) - public void txRenewDeniedIfEnvClosed() { - final Txn txnRead = env.txnRead(); - txnRead.close(); - env.close(); - txnRead.renew(); + @Test + void txRenewDeniedIfEnvClosed() { + assertThatThrownBy( + () -> { + final Txn txnRead = env.txnRead(); + txnRead.close(); + env.close(); + txnRead.renew(); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = AlreadyClosedException.class) - public void txCloseDeniedIfEnvClosed() { - final Txn txnRead = env.txnRead(); - env.close(); - txnRead.close(); + @Test + void txCloseDeniedIfEnvClosed() { + assertThatThrownBy( + () -> { + final Txn txnRead = env.txnRead(); + env.close(); + txnRead.close(); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = AlreadyClosedException.class) - public void txCommitDeniedIfEnvClosed() { - final Txn txnRead = env.txnRead(); - env.close(); - txnRead.commit(); + @Test + void txCommitDeniedIfEnvClosed() { + assertThatThrownBy( + () -> { + final Txn txnRead = env.txnRead(); + env.close(); + txnRead.commit(); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = AlreadyClosedException.class) - public void txAbortDeniedIfEnvClosed() { - final Txn txnRead = env.txnRead(); - env.close(); - txnRead.abort(); + @Test + void txAbortDeniedIfEnvClosed() { + assertThatThrownBy( + () -> { + final Txn txnRead = env.txnRead(); + env.close(); + txnRead.abort(); + }) + .isInstanceOf(AlreadyClosedException.class); } - @Test(expected = AlreadyClosedException.class) - public void txResetDeniedIfEnvClosed() { - final Txn txnRead = env.txnRead(); - env.close(); - txnRead.reset(); + @Test + void txResetDeniedIfEnvClosed() { + assertThatThrownBy( + () -> { + final Txn txnRead = env.txnRead(); + env.close(); + txnRead.reset(); + }) + .isInstanceOf(AlreadyClosedException.class); } @Test public void txParent() { try (Txn txRoot = env.txnWrite(); - Txn txChild = env.txn(txRoot)) { - assertThat(txRoot.getParent(), is(nullValue())); - assertThat(txChild.getParent(), is(txRoot)); + Txn txChild = env.txn(txRoot)) { + assertThat(txRoot.getParent()).isNull(); + assertThat(txChild.getParent()).isEqualTo(txRoot); } } - @Test(expected = AlreadyClosedException.class) - public void txParentDeniedIfEnvClosed() { - try (Txn txRoot = env.txnWrite(); - Txn txChild = env.txn(txRoot)) { - env.close(); - assertThat(txChild.getParent(), is(txRoot)); + @Test + public void txParent2() { + try (Txn txRoot = env.txnWrite()) { + assertThatThrownBy( + () -> { + env.txn(txRoot, (TxnFlagSet) null); + }) + .isInstanceOf(NullPointerException.class); } } - @Test(expected = IncompatibleParent.class) - public void txParentROChildRWIncompatible() { - try (Txn txRoot = env.txnRead()) { - env.txn(txRoot); // error + @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(expected = IncompatibleParent.class) - public void txParentRWChildROIncompatible() { - try (Txn txRoot = env.txnWrite()) { - env.txn(txRoot, MDB_RDONLY_TXN); // error - } + @Test + void txParentDeniedIfEnvClosed() { + assertThatThrownBy( + () -> { + try (Txn txRoot = env.txnWrite(); + Txn txChild = env.txn(txRoot)) { + env.close(); + assertThat(txChild.getParent()).isEqualTo(txRoot); + } + }) + .isInstanceOf(AlreadyClosedException.class); } @Test - public void txReadOnly() { + void txParentROChildRWIncompatible() { + assertThatThrownBy( + () -> { + try (Txn txRoot = env.txnRead()) { + env.txn(txRoot); // error + } + }) + .isInstanceOf(IncompatibleParent.class); + } + + @Test + void txParentRWChildROIncompatible() { + assertThatThrownBy( + () -> { + try (Txn txRoot = env.txnWrite()) { + env.txn(txRoot, MDB_RDONLY_TXN); // error + } + }) + .isInstanceOf(IncompatibleParent.class); + } + + @Test + void txReadOnly() { try (Txn txn = env.txnRead()) { - assertThat(txn.getParent(), is(nullValue())); - assertThat(txn.getState(), is(READY)); - assertThat(txn.isReadOnly(), is(true)); + assertThat(txn.getParent()).isNull(); + assertThat(txn.getState()).isEqualTo(READY); + assertThat(txn.isReadOnly()).isTrue(); txn.checkReady(); txn.checkReadOnly(); txn.reset(); - assertThat(txn.getState(), is(RESET)); + assertThat(txn.getState()).isEqualTo(RESET); txn.renew(); - assertThat(txn.getState(), is(READY)); + assertThat(txn.getState()).isEqualTo(READY); txn.commit(); - assertThat(txn.getState(), is(DONE)); + assertThat(txn.getState()).isEqualTo(DONE); txn.close(); - assertThat(txn.getState(), is(RELEASED)); + assertThat(txn.getState()).isEqualTo(RELEASED); } } @Test - public void txReadWrite() { + void txReadWrite() { final Txn txn = env.txnWrite(); - assertThat(txn.getParent(), is(nullValue())); - assertThat(txn.getState(), is(READY)); - assertThat(txn.isReadOnly(), is(false)); + assertThat(txn.getParent()).isNull(); + assertThat(txn.getState()).isEqualTo(READY); + assertThat(txn.isReadOnly()).isFalse(); txn.checkReady(); txn.checkWritesAllowed(); txn.commit(); - assertThat(txn.getState(), is(DONE)); + assertThat(txn.getState()).isEqualTo(DONE); txn.close(); - assertThat(txn.getState(), is(RELEASED)); + assertThat(txn.getState()).isEqualTo(RELEASED); } - @Test(expected = NotResetException.class) - public void txRenewDeniedWithoutPriorReset() { - try (Txn txn = env.txnRead()) { - txn.renew(); - } + @Test + void txRenewDeniedWithoutPriorReset() { + assertThatThrownBy( + () -> { + try (Txn txn = env.txnRead()) { + txn.renew(); + } + }) + .isInstanceOf(NotResetException.class); } - @Test(expected = ResetException.class) - public void txResetDeniedForAlreadyResetTransaction() { - try (Txn txn = env.txnRead()) { - txn.reset(); - txn.renew(); - txn.reset(); - txn.reset(); - } + @Test + void txResetDeniedForAlreadyResetTransaction() { + assertThatThrownBy( + () -> { + try (Txn txn = env.txnRead()) { + txn.reset(); + txn.renew(); + txn.reset(); + txn.reset(); + } + }) + .isInstanceOf(ResetException.class); } - @Test(expected = ReadOnlyRequiredException.class) - public void txResetDeniedForReadWriteTransaction() { - try (Txn txn = env.txnWrite()) { - txn.reset(); - } + @Test + void txResetDeniedForReadWriteTransaction() { + assertThatThrownBy( + () -> { + try (Txn txn = env.txnWrite()) { + txn.reset(); + } + }) + .isInstanceOf(ReadOnlyRequiredException.class); } - @Test(expected = BadValueSizeException.class) - public void zeroByteKeysRejected() throws IOException { - final Dbi dbi = env.openDbi(DB_1, MDB_CREATE); - final ByteBuffer key = allocateDirect(4); - key.putInt(1); - assertThat(key.remaining(), is(0)); // because key.flip() skipped - dbi.put(key, bb(2)); + @Test + void zeroByteKeysRejected() { + assertThatThrownBy( + () -> { + 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 + dbi.put(key, bb(2)); + }) + .isInstanceOf(BadValueSizeException.class); } - } diff --git a/src/test/java/org/lmdbjava/VerifierTest.java b/src/test/java/org/lmdbjava/VerifierTest.java index 41f4e774..ee396084 100644 --- a/src/test/java/org/lmdbjava/VerifierTest.java +++ b/src/test/java/org/lmdbjava/VerifierTest.java @@ -1,60 +1,62 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% */ package org.lmdbjava; -import static com.jakewharton.byteunits.BinaryByteUnit.MEBIBYTES; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.greaterThan; +import static org.assertj.core.api.Assertions.assertThat; import static org.lmdbjava.Env.create; import static org.lmdbjava.EnvFlags.MDB_NOSUBDIR; -import java.io.File; -import java.io.IOException; 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; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; - -/** - * Test {@link Verifier}. - */ +/** Test {@link Verifier}. */ public final class VerifierTest { - @Rule - public final TemporaryFolder tmp = new TemporaryFolder(); - @Test - public void verification() throws IOException { - final File path = tmp.newFile(); - try (Env env = create() - .setMaxReaders(1) - .setMaxDbs(Verifier.DBI_COUNT) - .setMapSize(MEBIBYTES.toBytes(10)) - .open(path, MDB_NOSUBDIR)) { - final Verifier v = new Verifier(env); - final int seconds = Integer.getInteger("verificationSeconds", 2); - assertThat(v.runFor(seconds, TimeUnit.SECONDS), greaterThan(1L)); + void verification() { + 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/java/org/lmdbjava/package-info.java b/src/test/java/org/lmdbjava/package-info.java index baaacbe8..950fc2db 100644 --- a/src/test/java/org/lmdbjava/package-info.java +++ b/src/test/java/org/lmdbjava/package-info.java @@ -1,24 +1,16 @@ -/*- - * #%L - * LmdbJava - * %% - * Copyright (C) 2016 - 2023 The LmdbJava Open Source Project - * %% +/* + * Copyright © 2016-2025 The LmdbJava Open Source Project + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * #L% - */ - -/** - * Lightning Memory Database (LMDB) for Java (LmdbJava) tests. */ package org.lmdbjava; 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