├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── dependabot.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── benches └── bench.rs ├── clippy.toml ├── justfile ├── src ├── aggregate.rs ├── blake3.rs ├── cdylib │ └── cdylib.rs ├── fnv.rs ├── lib.rs ├── md5.rs ├── scalar.rs ├── sha1.rs ├── sha224.rs ├── sha256.rs ├── sha384.rs ├── sha512.rs ├── state.rs └── xxhash.rs └── tests ├── _utils.rs ├── aggregate.rs ├── scalar.rs └── test-ext.sh /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | # Maintain dependencies for GitHub Actions 5 | - package-ecosystem: github-actions 6 | directory: "/" 7 | schedule: 8 | interval: weekly 9 | groups: 10 | all-actions-version-updates: 11 | applies-to: version-updates 12 | patterns: 13 | - "*" 14 | all-actions-security-updates: 15 | applies-to: security-updates 16 | patterns: 17 | - "*" 18 | 19 | # Update Rust dependencies 20 | - package-ecosystem: cargo 21 | directory: "/" 22 | schedule: 23 | interval: daily 24 | time: "02:00" 25 | open-pull-requests-limit: 10 26 | groups: 27 | all-cargo-version-updates: 28 | applies-to: version-updates 29 | patterns: 30 | - "*" 31 | all-cargo-security-updates: 32 | applies-to: security-updates 33 | patterns: 34 | - "*" 35 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | release: 9 | types: [ published ] 10 | workflow_dispatch: 11 | 12 | defaults: 13 | run: 14 | shell: bash 15 | 16 | jobs: 17 | test: 18 | name: Test 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - if: github.event_name != 'release' && github.event_name != 'workflow_dispatch' 23 | uses: Swatinem/rust-cache@v2 24 | - uses: taiki-e/install-action@v2 25 | with: { tool: just } 26 | - if: github.event_name == 'release' 27 | name: Ensure this crate has not yet been published (on release) 28 | run: just check-if-published 29 | - run: just ci-test 30 | - name: Check semver 31 | uses: obi1kenobi/cargo-semver-checks-action@v2 32 | 33 | test-msrv: 34 | name: Test MSRV 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | - if: github.event_name != 'release' && github.event_name != 'workflow_dispatch' 39 | uses: Swatinem/rust-cache@v2 40 | - uses: taiki-e/install-action@v2 41 | with: { tool: just } 42 | - name: Read MSRV 43 | id: msrv 44 | run: echo "value=$(just get-msrv)" >> $GITHUB_OUTPUT 45 | - name: Install MSRV Rust ${{ steps.msrv.outputs.value }} 46 | uses: dtolnay/rust-toolchain@stable 47 | with: 48 | toolchain: ${{ steps.msrv.outputs.value }} 49 | - run: just ci_mode=0 ci-test-msrv # Ignore warnings in MSRV 50 | 51 | coverage: 52 | name: Code Coverage 53 | if: github.event_name != 'release' 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@v4 57 | - uses: Swatinem/rust-cache@v2 58 | - uses: taiki-e/install-action@v2 59 | with: { tool: 'just,cargo-llvm-cov' } 60 | - name: Generate code coverage 61 | run: just ci-coverage 62 | - name: Upload coverage to Codecov 63 | uses: codecov/codecov-action@v5 64 | with: 65 | token: ${{ secrets.CODECOV_TOKEN }} 66 | files: target/llvm-cov/codecov.info 67 | fail_ci_if_error: true 68 | 69 | build: 70 | name: Build ${{ matrix.target }} 71 | needs: [ test ] 72 | runs-on: ${{ matrix.os }} 73 | strategy: 74 | fail-fast: false 75 | matrix: 76 | include: 77 | - target: aarch64-apple-darwin 78 | file: libsqlite_hashes.dylib 79 | download: 'https://www.sqlite.org/2023/sqlite-tools-osx-x64-3440200.zip' 80 | os: macOS-latest 81 | sqlite3: ./sqlite3 82 | - target: x86_64-apple-darwin 83 | file: libsqlite_hashes.dylib 84 | download: 'https://www.sqlite.org/2023/sqlite-tools-osx-x64-3440200.zip' 85 | os: macOS-latest 86 | sqlite3: ./sqlite3 87 | - target: x86_64-pc-windows-msvc 88 | file: sqlite_hashes.dll 89 | download: 'https://www.sqlite.org/2023/sqlite-tools-win-x64-3440200.zip' 90 | os: windows-latest 91 | sqlite3: ./sqlite3.exe 92 | - target: x86_64-unknown-linux-gnu 93 | file: libsqlite_hashes.so 94 | os: ubuntu-latest 95 | sqlite3: sqlite3 96 | steps: 97 | - uses: actions/checkout@v4 98 | - if: github.event_name != 'release' && github.event_name != 'workflow_dispatch' 99 | uses: Swatinem/rust-cache@v2 100 | - uses: taiki-e/install-action@v2 101 | with: { tool: just } 102 | - if: matrix.download 103 | name: Download SQLite 104 | uses: carlosperate/download-file-action@v2 105 | with: 106 | file-url: '${{ matrix.download }}' 107 | file-name: sqlite.zip 108 | location: ./tmp-downloads 109 | - if: matrix.download 110 | name: Install SQLite 111 | run: | 112 | cd tmp-downloads 113 | if [[ "${{ runner.os }}" == "Windows" ]]; then 114 | 7z x sqlite.zip 115 | else 116 | unzip sqlite.zip 117 | chmod +x ${{ matrix.sqlite3 }} 118 | fi 119 | mv ${{ matrix.sqlite3 }} ../ 120 | cd .. 121 | rm -rf ./tmp-downloads 122 | - name: SQLite Info 123 | run: | 124 | which ${{ matrix.sqlite3 }} 125 | ${{ matrix.sqlite3 }} --version 126 | ${{ matrix.sqlite3 }} <"] 6 | repository = "https://github.com/nyurik/sqlite-hashes" 7 | edition = "2021" 8 | license = "MIT OR Apache-2.0" 9 | keywords = ["sqlite", "rusqlite", "hash", "md5", "sha256"] 10 | categories = ["database", "cryptography"] 11 | rust-version = "1.82" 12 | 13 | [lib] 14 | name = "sqlite_hashes" 15 | 16 | # Loadable extension is a cdylib (lib), but Rust does not allow multiple libs per crate, so using an example instead. 17 | # See https://github.com/rust-lang/cargo/issues/8628 18 | [[example]] 19 | name = "sqlite_hashes" 20 | path = "src/cdylib/cdylib.rs" 21 | crate-type = ["cdylib"] 22 | required-features = ["loadable_extension"] 23 | 24 | [features] 25 | default = ["trace", "aggregate", "hex", "md5", "sha1", "sha224", "sha256", "sha384", "sha512", "blake3", "fnv", "xxhash"] 26 | # Use this feature to build a loadable extension. 27 | # Assumes --no-default-features. 28 | default_loadable_extension = ["loadable_extension", "aggregate", "hex", "md5", "sha1", "sha224", "sha256", "sha384", "sha512", "blake3", "fnv", "xxhash"] 29 | # 30 | # Enable Trace Logging 31 | trace = ["dep:hex", "dep:log"] 32 | # 33 | # Enable HEX-outputing variants like *_hex() and *_concat_hex() (with "aggregate" feature) 34 | hex = ["dep:hex"] 35 | # 36 | # Enable aggregate functions 37 | aggregate = [] 38 | # 39 | # "rusqlite/bundled", "rusqlite/modern_sqlite" 40 | # 41 | # Build loadable extension. 42 | # See https://github.com/rusqlite/rusqlite/discussions/1423 43 | # This feature does not work with "rusqlite/modern_sqlite" 44 | loadable_extension = ["rusqlite/loadable_extension", "rusqlite/trace"] 45 | # 46 | # Hashing algorithms 47 | md5 = ["dep:md-5"] 48 | sha1 = ["dep:sha1"] 49 | sha224 = ["dep:sha2"] 50 | sha256 = ["dep:sha2"] 51 | sha384 = ["dep:sha2"] 52 | sha512 = ["dep:sha2"] 53 | blake3 = ["dep:blake3"] 54 | fnv = ["dep:noncrypto-digests", "noncrypto-digests?/fnv"] 55 | xxhash = ["dep:noncrypto-digests", "noncrypto-digests?/xxh3", "noncrypto-digests?/xxh32", "noncrypto-digests?/xxh64"] 56 | 57 | [dependencies] 58 | hex = { version = "0.4", optional = true } 59 | log = { version = "0.4.27", optional = true } 60 | 61 | # There are multiple versions that could work, but sqlx requires a specific one, so don't limit it here 62 | # Note that cdylib requires >= 0.32.0 (controlled by the lock file) 63 | # The `set-min-rusqlite-version` just recipe will parse the minimum version from here, so it must be 3 part 64 | rusqlite = { version = ">=0.30.0", features = ["functions"] } 65 | 66 | # Digest and all hashing algorithms are using the same crates internally, so should be kept in sync 67 | digest = "0.10.7" 68 | blake3 = { version = "1.8.2", features = ["traits-preview"], optional = true } 69 | md-5 = { version = "0.10.6", optional = true } 70 | sha1 = { version = "0.10.6", optional = true } 71 | sha2 = { version = "0.10.9", optional = true } 72 | noncrypto-digests = { version = "0.3.3", optional = true } 73 | 74 | [dev-dependencies] 75 | criterion = { version = "0.6", features = ["html_reports"] } 76 | ctor = "0.4" 77 | env_logger = "0.11" 78 | insta = { version = "1", features = [] } 79 | 80 | [lints.rust] 81 | unused_qualifications = "warn" 82 | 83 | [lints.clippy] 84 | cargo = { level = "warn", priority = -1 } 85 | pedantic = { level = "warn", priority = -1 } 86 | missing_errors_doc = "allow" 87 | module_name_repetitions = "allow" 88 | 89 | [[bench]] 90 | name = "bench" 91 | harness = false 92 | 93 | #[patch.crates-io] 94 | #rusqlite = { path = "../rusqlite" } 95 | #libsqlite3-sys = { path = "../rusqlite/libsqlite3-sys" } 96 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2025 Yuri Astrakhan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sqlite-hashes 2 | 3 | [![GitHub](https://img.shields.io/badge/github-sqlite--hashes-8da0cb?logo=github)](https://github.com/nyurik/sqlite-hashes) 4 | [![crates.io version](https://img.shields.io/crates/v/sqlite-hashes.svg)](https://crates.io/crates/sqlite-hashes) 5 | [![docs.rs docs](https://docs.rs/sqlite-hashes/badge.svg)](https://docs.rs/sqlite-hashes) 6 | [![license](https://img.shields.io/crates/l/sqlite-hashes.svg)](https://github.com/nyurik/sqlite-hashes/blob/main/LICENSE-APACHE) 7 | [![CI build](https://github.com/nyurik/sqlite-hashes/actions/workflows/ci.yml/badge.svg)](https://github.com/nyurik/sqlite-hashes/actions) 8 | [![Codecov](https://img.shields.io/codecov/c/github/nyurik/sqlite-hashes)](https://app.codecov.io/gh/nyurik/sqlite-hashes) 9 | 10 | Implement `SQLite` hashing functions with aggregation support, including MD5, SHA1, SHA224, SHA256, SHA384, SHA512, 11 | FNV-1a, xxHash. Functions are available as a loadable extension, or as a Rust library. 12 | 13 | See also [SQLite-compressions](https://github.com/nyurik/sqlite-compressions) extension for gzip, brotli, and bsdiff support. 14 | 15 | ## Usage 16 | 17 | This `SQLite` extension adds hashing functions like `sha256(...)`, `sha256_hex(...)`, `sha256_concat` 18 | and `sha256_concat_hex` for multiple hashing algorithms. The `sha256` and `sha256_concat` function returns a blob value, 19 | while the `*_hex` return a HEX string similar to `SQLite`'s own `hex()` function. 20 | 21 | Functions support any number of arguments, e.g. `sha256('foo', 'bar', 'baz')`, hashing them in order as if they were 22 | concatenated. Functions can hash text and blob values, but will raise an error on other types like integers and floating 23 | point numbers. All `NULL` values are ignored. When calling the built-in `SQLite`'s `hex(NULL)`, the result is an empty 24 | string, so `sha256_hex(NULL)` will return an empty string as well to be consistent. 25 | 26 | The `*_concat` functions support aggregate to compute combined hash over a set of values like a column in a table, 27 | e.g. `sha256_concat` and `sha256_concat_hex`. Just like scalar functions, multiple arguments are also supported, so you 28 | can compute a hash over a set of columns, e.g. `sha256_concat(col1, col2, col3)`. 29 | 30 | ### Extension 31 | 32 | To use as an extension, load the `libsqlite_hashes.so` shared library into `SQLite`. 33 | 34 | ```bash 35 | $ sqlite3 36 | sqlite> .load ./libsqlite_hashes 37 | sqlite> SELECT md5_hex('Hello world!'); 38 | 86FB269D190D2C85F6E0468CECA42A20 39 | ``` 40 | 41 | ### PHP 42 | 43 | To load the extension from PHP, store `libsqlite_hashes.so` in your `SQLite` extensions folder. If you don't have any extensions yet, you can determine your `SQLite` extensions folder by defining the `sqlite3.extension_dir` variable as the filepath to the extensions directory in your `php.ini` file. 44 | 45 | ```ini 46 | sqlite3.extension_dir = /path/to/sqlite/extensions 47 | ``` 48 | 49 | Obs¹: To locate your `php.ini` file refer to the following [post in StackOverflow](https://stackoverflow.com/questions/8684609/where-can-i-find-php-ini). 50 | 51 | Obs²: To verify if your changes to `php.ini` took effect, you can run `phpinfo();` in PHP as follows: 52 | 53 | ```bash 54 | $ php -r 'phpinfo();' | grep sqlite 55 | Configure Command => './configure' '--build=x86_64-linux-gnu' '--with-config-file-path=/usr/local/etc/php' '--with-config-file-scan-dir=/usr/local/etc/php/conf.d' '--enable-option-checking=fatal' '--with-mhash' '--with-pic' '--enable-mbstring' '--enable-mysqlnd' '--with-password-argon2' '--with-sodium=shared' '--with-pdo-sqlite=/usr' '--with-sqlite3=/usr' '--with-curl' '--with-iconv' '--with-openssl' '--with-readline' '--with-zlib' '--disable-phpdbg' '--with-pear' '--with-libdir=lib/x86_64-linux-gnu' '--disable-cgi' '--with-apxs2' 'build_alias=x86_64-linux-gnu' 'PHP_UNAME=Linux - Docker' 'PHP_BUILD_PROVIDER=https://github.com/docker-library/php' 56 | PDO drivers => sqlite 57 | pdo_sqlite 58 | sqlite3 59 | sqlite3.defensive => On => On 60 | sqlite3.extension_dir => /var/www/extensions/ => /var/www/extensions/ 61 | ``` 62 | 63 | Now, make sure to always load the extension by running `SQLite3::loadExtension('libsqlite_hashes.so');` (remember to include the correct file extension according to your Operating System, e.g., `.so` for Linux, `.dll` for Windows, and `.dylib` for macOS) in each project you intend to use sqlite hash functions. 64 | 65 | For instance, the following code snippet demonstrate the minimum required code to run SHA256 in a string "password": 66 | 67 | ```php 68 | loadExtension('libsqlite_hashes.so'); 71 | echo $db->querySingle("SELECT hex(sha256('password'))"); 72 | ``` 73 | 74 | Warning: According to , PHP 7 fails to load the extension with or without specifying the file extension as it ends up looking for a file with a double extension (e.g., `libsqlite_hashes.so.so`). Therefore, we recommend using it with PHP 8. 75 | 76 | ### Rust library 77 | 78 | To use as a Rust library, add `sqlite-hashes` to your `Cargo.toml` dependencies. Then, register the needed functions 79 | with `register_hash_functions(&db)`. This will register all available functions, or you can 80 | use `register_md5_functions(&db)` or `register_sha256_functions(&db)` to register just the needed ones (you may also 81 | disable the default features to reduce compile time and binary size). 82 | 83 | ```rust 84 | use sqlite_hashes::{register_hash_functions, rusqlite::Connection}; 85 | 86 | // Connect to SQLite DB and register needed hashing functions 87 | let db = Connection::open_in_memory().unwrap(); 88 | // can also use hash-specific ones like register_sha256_functions(&db) 89 | register_hash_functions(&db).unwrap(); 90 | 91 | // Hash 'password' using SHA-256, and dump resulting BLOB as a HEX string 92 | let sql = "SELECT hex(sha256('password'));"; 93 | let hash: String = db.query_row_and_then(&sql, [], |r| r.get(0)).unwrap(); 94 | assert_eq!(hash, "5E884898DA28047151D0E56F8DC6292773603D0D6AABBDD62A11EF721D1542D8"); 95 | 96 | // Same as above, but use sha256_hex() function to dump the result as a HEX string directly 97 | let sql = "SELECT sha256_hex('password');"; 98 | let hash: String = db.query_row_and_then(&sql, [], |r| r.get(0)).unwrap(); 99 | assert_eq!(hash, "5E884898DA28047151D0E56F8DC6292773603D0D6AABBDD62A11EF721D1542D8"); 100 | 101 | // Hash 'pass' (as text) and 'word' (as blob) using SHA-256, and dump it as a HEX string 102 | // The result is the same as the above 'password' example. 103 | let sql = "SELECT sha256_hex(cast('pass' as text), cast('word' as blob));"; 104 | let hash: String = db.query_row_and_then(&sql, [], |r| r.get(0)).unwrap(); 105 | assert_eq!(hash, "5E884898DA28047151D0E56F8DC6292773603D0D6AABBDD62A11EF721D1542D8"); 106 | ``` 107 | 108 | ### Aggregate Functions 109 | 110 | When `aggregate` feature is enabled (default), there are functions to compute combined hash over a set of 111 | values like a column in a table, e.g. `sha256_concat` and `sha256_concat_hex`. Just like scalar functions, multiple 112 | arguments are also supported, so you can compute a hash over a set of columns, e.g. `sha256_concat(col1, col2, col3)`. 113 | 114 | #### IMPORTANT NOTE: ORDERING 115 | 116 | `SQLite` does NOT guarantee the order of rows when executing aggregate functions. A 117 | query `SELECT sha256_concat(v) FROM tbl ORDER BY v;` will NOT concatenate values in sorted order, but will use some 118 | internal storage order instead. 119 | 120 | `SQLite` [v3.44.0](https://www.sqlite.org/changes.html#version_3_44_0)(2023-11-01) added support for the 121 | `ORDER BY` clause 122 | **inside** the aggregate function call, e.g. `SELECT sha256_concat(v ORDER BY v) FROM tbl;`. Make sure to use that to 123 | guarantee consistent results. 124 | 125 | For older `SQLite` versions, one common workaround was to use a subquery, 126 | e.g. `SELECT group_concat(v) FROM (SELECT v FROM tbl ORDER BY v);`. This is 127 | NOT guaranteed to work in future versions of `SQLite`. See [discussion](https://sqlite.org/forum/info/a49d9c4083b5350c) 128 | for more details. 129 | 130 | #### Using with `SQLx` 131 | 132 | To use with [SQLx](https://crates.io/crates/sqlx), you need to get the raw handle from the 133 | `SqliteConnection` and pass it to the registration function. 134 | 135 | ```rust,ignore 136 | use rusqlite::Connection; 137 | use sqlite_hashes::register_hash_functions; 138 | use sqlx::sqlite::SqliteConnection; 139 | 140 | async fn register_functions(sqlx_conn: &mut SqliteConnection) { 141 | // SAFETY: No query must be performed on `sqlx_conn` until `handle_lock` is dropped. 142 | let mut handle_lock = sqlx_conn.lock_handle().await.unwrap(); 143 | let handle = handle_lock.as_raw_handle().as_ptr(); 144 | 145 | // SAFETY: this is safe as long as handle_lock is valid. 146 | let rusqlite_conn = unsafe { Connection::from_handle(handle) }.unwrap(); 147 | 148 | // Registration is attached to the connection, not to rusqlite_conn, 149 | // so it will be available for the entire lifetime of the `sqlx_conn`. 150 | // Registration will be automatically dropped when SqliteConnection is dropped. 151 | register_hash_functions(&rusqlite_conn).unwrap(); 152 | } 153 | ``` 154 | 155 | ## Crate features 156 | 157 | By default, this crate will compile with all features. You can enable just the ones you need to reduce compile time and 158 | binary size. 159 | 160 | ```toml 161 | [dependencies] 162 | sqlite-hashes = { version = "0.10", default-features = false, features = ["hex", "aggregate", "sha256"] } 163 | ``` 164 | 165 | * **trace** - enable tracing support, logging all function calls and their arguments 166 | * **hex** - enable hex string functions like `*_hex()` and `*_concat_hex()` (if `aggregate` is enabled) 167 | * **aggregate** - enable aggregate functions like `*_concat()` and `*_concat_hex()` (if `hex` is enabled) 168 | * **md5** - enable MD5 hash support 169 | * **sha1** - enable SHA1 hash support 170 | * **sha224** - enable SHA224 hash support 171 | * **sha256** - enable SHA256 hash support 172 | * **sha384** - enable SHA384 hash support 173 | * **sha512** - enable SHA512 hash support 174 | * **blake3** - enable BLAKE3 hash support 175 | * **fnv** - enable FNV-1a hash support 176 | * **xxhash** - enable `xxh32, xxh64, xxh3_64, xxh3_128` hash support 177 | 178 | The **`loadable_extension`** feature should only be used when building 179 | a `.so` / `.dylib` / `.dll` extension file that can be loaded directly into sqlite3 executable. 180 | 181 | ## Development 182 | 183 | * You must install `sqlite3` and `libsqlite3-dev`, e.g. `sudo apt install -y libsqlite3-dev sqlite3` on Ubuntu/Mint. 184 | * This project is easier to develop with [just](https://github.com/casey/just#readme), a modern alternative to `make`. 185 | Install it with `cargo install just`. 186 | * To get a list of available commands, run `just`. 187 | * To run tests, use `just test`. 188 | 189 | ## License 190 | 191 | Licensed under either of 192 | 193 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) 194 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or ) 195 | at your option. 196 | 197 | ### Contribution 198 | 199 | Unless you explicitly state otherwise, any contribution intentionally 200 | submitted for inclusion in the work by you, as defined in the 201 | Apache-2.0 license, shall be dual-licensed as above, without any 202 | additional terms or conditions. 203 | -------------------------------------------------------------------------------- /benches/bench.rs: -------------------------------------------------------------------------------- 1 | use criterion::measurement::WallTime; 2 | use criterion::{criterion_group, criterion_main, BenchmarkGroup, BenchmarkId, Criterion}; 3 | use sqlite_hashes::{HashState, NamedDigest}; 4 | 5 | criterion_group!(benches, all_hash_tests); 6 | criterion_main!(benches); 7 | 8 | fn all_hash_tests(c: &mut Criterion) { 9 | let mut group = c.benchmark_group("hashes"); 10 | hash_test::(&mut group); 11 | hash_test::(&mut group); 12 | hash_test::(&mut group); 13 | hash_test::(&mut group); 14 | hash_test::(&mut group); 15 | hash_test::(&mut group); 16 | hash_test::(&mut group); 17 | hash_test::(&mut group); 18 | hash_test::(&mut group); 19 | hash_test::(&mut group); 20 | hash_test::(&mut group); 21 | group.finish(); 22 | } 23 | 24 | fn hash_test(group: &mut BenchmarkGroup) { 25 | for size in [10, 10 * 1024, 1024 * 1024] { 26 | let data = gen_data(size); 27 | group.bench_function(BenchmarkId::new(T::name(), size), |b| { 28 | b.iter(|| { 29 | let mut state = HashState::::default(); 30 | state.add_value(data.as_slice()); 31 | state.finalize() 32 | }); 33 | }); 34 | } 35 | } 36 | 37 | fn gen_data(size: usize) -> Vec { 38 | let mut byte_data: Vec = Vec::with_capacity(size); 39 | for i in 0..size { 40 | #[allow(clippy::cast_possible_truncation)] 41 | byte_data.push((i % 256) as u8); 42 | } 43 | byte_data 44 | } 45 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | allow-unwrap-in-tests = true 2 | avoid-breaking-exported-api = false 3 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env just --justfile 2 | 3 | main_crate := 'sqlite-hashes' 4 | features_flag := '--all-features' 5 | bin_name := snakecase(main_crate) 6 | sqlite3 := 'sqlite3' 7 | 8 | # if running in CI, treat warnings as errors by setting RUSTFLAGS and RUSTDOCFLAGS to '-D warnings' unless they are already set 9 | # Use `CI=true just ci-test` to run the same tests as in GitHub CI. 10 | # Use `just env-info` to see the current values of RUSTFLAGS and RUSTDOCFLAGS 11 | ci_mode := if env('CI', '') != '' {'1'} else {''} 12 | export RUSTFLAGS := env('RUSTFLAGS', if ci_mode == '1' {'-D warnings'} else {''}) 13 | export RUSTDOCFLAGS := env('RUSTDOCFLAGS', if ci_mode == '1' {'-D warnings'} else {''}) 14 | export RUST_BACKTRACE := env('RUST_BACKTRACE', if ci_mode == '1' {'1'} else {''}) 15 | 16 | @_default: 17 | {{just_executable()}} --list 18 | 19 | # Run benchmarks 20 | bench: 21 | cargo bench 22 | open target/criterion/report/index.html 23 | 24 | # Run integration tests and save its output as the new expected output 25 | bless *args: (cargo-install 'cargo-insta') 26 | cargo insta test --accept --unreferenced=delete {{args}} 27 | 28 | # Build the project 29 | build: build-lib build-ext 30 | 31 | # Build extension binary 32 | build-ext *args: 33 | cargo build --example {{bin_name}} --no-default-features --features default_loadable_extension {{args}} 34 | 35 | # Build the lib 36 | build-lib: 37 | cargo build --workspace 38 | 39 | # Quick compile without building a binary 40 | check: 41 | cargo check --workspace --all-targets {{features_flag}} 42 | 43 | # Verify that the current version of the crate is not the same as the one published on crates.io 44 | check-if-published package=main_crate: (assert-cmd 'jq') 45 | #!/usr/bin/env bash 46 | set -euo pipefail 47 | LOCAL_VERSION="$({{just_executable()}} get-crate-field version package)" 48 | echo "Detected crate {{package}} version: '$LOCAL_VERSION'" 49 | PUBLISHED_VERSION="$(cargo search --quiet {{package}} | grep "^{{package}} =" | sed -E 's/.* = "(.*)".*/\1/')" 50 | echo "Published crate version: '$PUBLISHED_VERSION'" 51 | if [ "$LOCAL_VERSION" = "$PUBLISHED_VERSION" ]; then 52 | echo "ERROR: The current crate version has already been published." 53 | exit 1 54 | else 55 | echo "The current crate version has not yet been published." 56 | fi 57 | 58 | # Quick compile - lib-only 59 | check-lib: 60 | cargo check --workspace 61 | 62 | # Generate code coverage report to upload to codecov.io 63 | ci-coverage: env-info && \ 64 | (coverage '--codecov --output-path target/llvm-cov/codecov.info') 65 | # ATTENTION: the full file path above is used in the CI workflow 66 | mkdir -p target/llvm-cov 67 | 68 | # Run all tests as expected by CI 69 | ci-test: env-info test-fmt check clippy test test-ext test-doc && assert-git-is-clean 70 | 71 | # Run minimal subset of tests to ensure compatibility with MSRV 72 | ci-test-msrv: env-info check-lib test 73 | 74 | # Clean all build artifacts 75 | clean: 76 | cargo clean 77 | 78 | # Run cargo clippy to lint the code 79 | clippy *args: 80 | cargo clippy --workspace --all-targets {{features_flag}} {{args}} 81 | cargo clippy --no-default-features --features default_loadable_extension {{args}} 82 | 83 | # Generate code coverage report. Will install `cargo llvm-cov` if missing. 84 | coverage *args='--no-clean --open': (cargo-install 'cargo-llvm-cov') 85 | cargo llvm-cov --workspace --all-targets --include-build-script {{args}} 86 | # TODO: add test coverage for the loadable extension too, and combine them 87 | # cargo llvm-cov --example {{bin_name}} --no-default-features --features default_loadable_extension --codecov --output-path codecov.info 88 | 89 | cross-build-ext *args: 90 | cross build --example {{bin_name}} --no-default-features --features default_loadable_extension {{args}} 91 | 92 | cross-build-ext-aarch64: (cross-build-ext '--target=aarch64-unknown-linux-gnu' '--release') 93 | 94 | cross-test-ext-aarch64: 95 | docker run \ 96 | --rm \ 97 | -v "$(pwd):/workspace" \ 98 | -w /workspace \ 99 | --entrypoint sh \ 100 | -e EXTENSION_FILE=target/aarch64-unknown-linux-gnu/release/examples/lib{{bin_name}} \ 101 | --platform linux/arm64 \ 102 | arm64v8/ubuntu \ 103 | -c 'apt-get update && apt-get install -y sqlite3 && tests/test-ext.sh' 104 | 105 | # Build and open code documentation 106 | docs *args='--open': 107 | DOCS_RS=1 cargo doc --no-deps {{args}} --workspace {{features_flag}} 108 | 109 | # Print environment info 110 | env-info: 111 | @echo "Running {{if ci_mode == '1' {'in CI mode'} else {'in dev mode'} }} on {{os()}} / {{arch()}}" 112 | {{just_executable()}} --version 113 | rustc --version 114 | cargo --version 115 | rustup --version 116 | @echo "RUSTFLAGS='$RUSTFLAGS'" 117 | @echo "RUSTDOCFLAGS='$RUSTDOCFLAGS'" 118 | 119 | # Reformat all code `cargo fmt`. If nightly is available, use it for better results 120 | fmt: 121 | #!/usr/bin/env bash 122 | set -euo pipefail 123 | if rustup component list --toolchain nightly | grep rustfmt &> /dev/null; then 124 | echo 'Reformatting Rust code using nightly Rust fmt to sort imports' 125 | cargo +nightly fmt --all -- --config imports_granularity=Module,group_imports=StdExternalCrate 126 | else 127 | echo 'Reformatting Rust with the stable cargo fmt. Install nightly with `rustup install nightly` for better results' 128 | cargo fmt --all 129 | fi 130 | 131 | # Get any package's field from the metadata 132 | get-crate-field field package=main_crate: 133 | cargo metadata --format-version 1 | jq -r '.packages | map(select(.name == "{{package}}")) | first | .{{field}}' 134 | 135 | # Get the minimum supported Rust version (MSRV) for the crate 136 | get-msrv: (get-crate-field 'rust_version') 137 | 138 | # Find the minimum supported Rust version (MSRV) using cargo-msrv extension, and update Cargo.toml 139 | msrv: (cargo-install 'cargo-msrv') 140 | cargo msrv find --write-msrv --ignore-lockfile 141 | 142 | # Check semver compatibility with prior published version. Install it with `cargo install cargo-semver-checks` 143 | semver *args: (cargo-install 'cargo-semver-checks') 144 | cargo semver-checks {{features_flag}} {{args}} 145 | 146 | # Switch to the minimum rusqlite version 147 | set-min-rusqlite-version: (assert-cmd 'jq') 148 | #!/usr/bin/env bash 149 | set -euo pipefail 150 | MIN_RUSQL_VER="$(grep '^rusqlite =.*version = ">=' Cargo.toml | sed -E 's/.*version = "[^"0-9]*([0-9.-]+).*/\1/')" 151 | echo "Switching to minimum rusqlite version: $MIN_RUSQL_VER" 152 | cargo update -p rusqlite --precise "$MIN_RUSQL_VER" 153 | 154 | # Run all unit and integration tests 155 | test: \ 156 | ( test-one-lib '--no-default-features' '--features' 'trace,hex,md5' ) \ 157 | ( test-one-lib '--no-default-features' '--features' 'trace,hex,sha1' ) \ 158 | ( test-one-lib '--no-default-features' '--features' 'trace,hex,sha224' ) \ 159 | ( test-one-lib '--no-default-features' '--features' 'trace,hex,sha256' ) \ 160 | ( test-one-lib '--no-default-features' '--features' 'trace,hex,sha384' ) \ 161 | ( test-one-lib '--no-default-features' '--features' 'trace,hex,sha512' ) \ 162 | ( test-one-lib '--no-default-features' '--features' 'trace,hex,blake3' ) \ 163 | ( test-one-lib '--no-default-features' '--features' 'trace,hex,fnv' ) \ 164 | ( test-one-lib '--no-default-features' '--features' 'trace,hex,xxhash' ) \ 165 | \ 166 | ( test-one-lib '--no-default-features' '--features' 'md5,sha1,sha224,sha256,sha384,sha512,blake3,fnv,xxhash' ) \ 167 | ( test-one-lib '--no-default-features' '--features' 'md5,sha1,sha224,sha256,sha384,sha512,blake3,fnv,xxhash,aggregate' ) \ 168 | \ 169 | ( test-one-lib '--no-default-features' '--features' 'md5,sha1,sha224,sha256,sha384,sha512,blake3,fnv,xxhash,hex' ) \ 170 | ( test-one-lib '--no-default-features' '--features' 'md5,sha1,sha224,sha256,sha384,sha512,blake3,fnv,xxhash,hex,aggregate' ) \ 171 | \ 172 | ( test-one-lib '--no-default-features' '--features' 'md5,sha1,sha224,sha256,sha384,sha512,blake3,fnv,xxhash,trace' ) \ 173 | ( test-one-lib '--no-default-features' '--features' 'md5,sha1,sha224,sha256,sha384,sha512,blake3,fnv,xxhash,trace,aggregate' ) \ 174 | \ 175 | ( test-one-lib '--no-default-features' '--features' 'md5,sha1,sha224,sha256,sha384,sha512,blake3,fnv,xxhash,hex,trace' ) \ 176 | ( test-one-lib '--no-default-features' '--features' 'md5,sha1,sha224,sha256,sha384,sha512,blake3,fnv,xxhash,hex,trace,aggregate' ) 177 | cargo test --doc # do not enable --all-features here as it will cause sqlite runtime errors 178 | 179 | # Test documentation generation 180 | test-doc: (docs '') 181 | 182 | # Test extension by loading it into sqlite and running SQL tests 183 | test-ext: build-ext 184 | ./tests/test-ext.sh 185 | 186 | # Test code formatting 187 | test-fmt: 188 | cargo fmt --all -- --check 189 | 190 | # Find unused dependencies. Install it with `cargo install cargo-udeps` 191 | udeps: (cargo-install 'cargo-udeps') 192 | cargo +nightly udeps --workspace --all-targets {{features_flag}} 193 | 194 | # Update all dependencies, including breaking changes. Requires nightly toolchain (install with `rustup install nightly`) 195 | update: 196 | cargo +nightly -Z unstable-options update --breaking 197 | cargo update 198 | 199 | # Ensure that a certain command is available 200 | [private] 201 | assert-cmd command: 202 | @if ! type {{command}} > /dev/null; then \ 203 | echo "Command '{{command}}' could not be found. Please make sure it has been installed on your computer." ;\ 204 | exit 1 ;\ 205 | fi 206 | 207 | # Make sure the git repo has no uncommitted changes 208 | [private] 209 | assert-git-is-clean: 210 | @if [ -n "$(git status --untracked-files --porcelain)" ]; then \ 211 | >&2 echo "ERROR: git repo is no longer clean. Make sure compilation and tests artifacts are in the .gitignore, and no repo files are modified." ;\ 212 | >&2 echo "######### git status ##########" ;\ 213 | git status ;\ 214 | exit 1 ;\ 215 | fi 216 | 217 | # Check if a certain Cargo command is installed, and install it if needed 218 | [private] 219 | cargo-install $COMMAND $INSTALL_CMD='' *args='': 220 | #!/usr/bin/env bash 221 | set -euo pipefail 222 | if ! command -v $COMMAND > /dev/null; then 223 | if ! command -v cargo-binstall > /dev/null; then 224 | echo "$COMMAND could not be found. Installing it with cargo install ${INSTALL_CMD:-$COMMAND} --locked {{args}}" 225 | cargo install ${INSTALL_CMD:-$COMMAND} --locked {{args}} 226 | else 227 | echo "$COMMAND could not be found. Installing it with cargo binstall ${INSTALL_CMD:-$COMMAND} --locked {{args}}" 228 | cargo binstall ${INSTALL_CMD:-$COMMAND} --locked {{args}} 229 | fi 230 | fi 231 | 232 | [private] 233 | is-sqlite3-available: 234 | if ! command -v {{sqlite3}} > /dev/null; then \ 235 | echo "{{sqlite3}} executable could not be found" ;\ 236 | exit 1 ;\ 237 | fi 238 | echo "Found {{sqlite3}} executable:" 239 | {{sqlite3}} --version 240 | 241 | [private] 242 | test-one-lib *args: 243 | @echo "### TEST {{args}} #######################################################################################################################" 244 | cargo test {{args}} 245 | -------------------------------------------------------------------------------- /src/aggregate.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "aggregate")] 2 | 3 | #[cfg(feature = "trace")] 4 | use std::borrow::Cow; 5 | use std::panic::{RefUnwindSafe, UnwindSafe}; 6 | 7 | use digest::Digest; 8 | #[cfg(feature = "trace")] 9 | use hex::ToHex as _; 10 | #[cfg(feature = "trace")] 11 | use log::trace; 12 | use rusqlite::functions::{Aggregate, Context, FunctionFlags}; 13 | use rusqlite::{Connection, ToSql}; 14 | 15 | use crate::rusqlite::types::{Type, ValueRef}; 16 | use crate::rusqlite::Error::{InvalidFunctionParameterType, InvalidParameterCount}; 17 | use crate::rusqlite::Result; 18 | use crate::state::HashState; 19 | 20 | #[cfg(not(feature = "trace"))] 21 | macro_rules! trace { 22 | ($($arg:tt)*) => {}; 23 | } 24 | 25 | pub fn create_agg_function(conn: &Connection, fn_name: &str, aggr: D) -> Result<()> 26 | where 27 | A: RefUnwindSafe + UnwindSafe, 28 | D: Aggregate + 'static, 29 | T: ToSql, 30 | { 31 | trace!("Registering aggregate function {fn_name}"); 32 | conn.create_aggregate_function( 33 | fn_name, 34 | -1, 35 | FunctionFlags::SQLITE_UTF8 36 | | FunctionFlags::SQLITE_DETERMINISTIC 37 | | FunctionFlags::SQLITE_DIRECTONLY, 38 | aggr, 39 | ) 40 | } 41 | 42 | pub struct AggType { 43 | #[cfg(feature = "trace")] 44 | fn_name: String, 45 | to_final: fn(HashState) -> Option, 46 | } 47 | 48 | impl AggType { 49 | pub fn new( 50 | #[cfg(feature = "trace")] fn_name: &str, 51 | to_final: fn(HashState) -> Option, 52 | ) -> Self { 53 | Self { 54 | #[cfg(feature = "trace")] 55 | fn_name: fn_name.to_ascii_uppercase(), 56 | to_final, 57 | } 58 | } 59 | } 60 | 61 | impl Aggregate, Option> 62 | for AggType 63 | { 64 | fn init(&self, _: &mut Context<'_>) -> Result> { 65 | trace!("{}: Aggregate::init", self.fn_name); 66 | // Keep track if any non-null values were added or not. 67 | // If there are, a non-null digest is returned. 68 | Ok(HashState::default()) 69 | } 70 | 71 | fn step(&self, ctx: &mut Context<'_>, agg: &mut HashState) -> Result<()> { 72 | let param_count = ctx.len(); 73 | if param_count == 0 { 74 | return Err(InvalidParameterCount(param_count, 1)); 75 | } 76 | for idx in 0..param_count { 77 | match ctx.get_raw(idx) { 78 | ValueRef::Blob(val) => { 79 | trace!("{}: arg{idx} -> step(blob {val:?})", self.fn_name); 80 | agg.add_value(val); 81 | } 82 | ValueRef::Text(val) => { 83 | trace!( 84 | "{}: arg{idx} -> step(text {:?})", 85 | self.fn_name, 86 | match std::str::from_utf8(val) { 87 | Ok(v) => Cow::from(v), 88 | Err(_) => Cow::from(val.encode_hex_upper::()), 89 | } 90 | ); 91 | agg.add_value(val); 92 | } 93 | ValueRef::Null => { 94 | trace!("{}: arg{idx} -> ignoring step(NULL)", self.fn_name); 95 | agg.add_null(); 96 | } 97 | ValueRef::Integer(_) => Err(InvalidFunctionParameterType(idx, Type::Integer))?, 98 | ValueRef::Real(_) => Err(InvalidFunctionParameterType(idx, Type::Real))?, 99 | } 100 | } 101 | Ok(()) 102 | } 103 | 104 | fn finalize(&self, _: &mut Context<'_>, agg: Option>) -> Result> { 105 | trace!("{}: Aggregate::finalize", self.fn_name); 106 | match agg { 107 | Some(agg) => Ok((self.to_final)(agg)), 108 | None => Ok(None), 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/blake3.rs: -------------------------------------------------------------------------------- 1 | use blake3::Hasher; 2 | 3 | use crate::rusqlite::{Connection, Result}; 4 | use crate::scalar::create_hash_fn; 5 | 6 | /// Register the `blake3` SQL function with the given `SQLite` connection. 7 | /// The function takes a single argument and returns the [BLAKE3 hash](https://en.wikipedia.org/wiki/BLAKE_(hash_function)#BLAKE3) (blob) of that argument. 8 | /// The argument can be either a string or a blob. 9 | /// If the argument is `NULL`, the result is `NULL`. 10 | /// 11 | /// # Example 12 | /// 13 | /// ``` 14 | /// # use sqlite_hashes::rusqlite::{Connection, Result}; 15 | /// # use sqlite_hashes::register_blake3_functions; 16 | /// # fn main() -> Result<()> { 17 | /// let db = Connection::open_in_memory()?; 18 | /// register_blake3_functions(&db)?; 19 | /// let hash: Vec = db.query_row("SELECT blake3('hello')", [], |r| r.get(0))?; 20 | /// let expected = b"\xea\x8f\x16\x3d\xb3\x86\x82\x92\x5e\x44\x91\xc5\xe5\x8d\x4b\xb3\x50\x6e\xf8\xc1\x4e\xb7\x8a\x86\xe9\x08\xc5\x62\x4a\x67\x20\x0f"; 21 | /// assert_eq!(hash, expected); 22 | /// # Ok(()) 23 | /// # } 24 | /// ``` 25 | pub fn register_blake3_functions(conn: &Connection) -> Result<()> { 26 | create_hash_fn::(conn, "blake3") 27 | } 28 | -------------------------------------------------------------------------------- /src/cdylib/cdylib.rs: -------------------------------------------------------------------------------- 1 | use std::os::raw::{c_char, c_int}; 2 | 3 | use rusqlite::ffi::SQLITE_NOTICE; 4 | use rusqlite::trace::log; 5 | use rusqlite::{ffi, Connection, Result}; 6 | 7 | /// This is the entry point for the `SQLite` extension. 8 | /// 9 | /// # Safety 10 | /// This function is unsafe because it interacts with raw pointers and the `SQLite` C API. 11 | #[no_mangle] 12 | pub unsafe extern "C" fn sqlite3_extension_init( 13 | db: *mut ffi::sqlite3, 14 | pz_err_msg: *mut *mut c_char, 15 | p_api: *mut ffi::sqlite3_api_routines, 16 | ) -> c_int { 17 | Connection::extension_init2(db, pz_err_msg, p_api, extension_init) 18 | } 19 | 20 | #[expect(clippy::needless_pass_by_value)] 21 | fn extension_init(db: Connection) -> Result { 22 | sqlite_hashes::register_hash_functions(&db)?; 23 | log(SQLITE_NOTICE, "Loaded sqlite_hashes extension"); 24 | Ok(false) 25 | } 26 | -------------------------------------------------------------------------------- /src/fnv.rs: -------------------------------------------------------------------------------- 1 | use noncrypto_digests::Fnv; 2 | 3 | use crate::rusqlite::{Connection, Result}; 4 | 5 | /// Register the `fnv1a` SQL function with the given `SQLite` connection. 6 | /// The `fnv1a` function uses [Fowler–Noll–Vo hash function](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function#FNV-1a_hash) to compute the hash of the argument(s). 7 | /// 8 | /// # Example 9 | /// 10 | /// ``` 11 | /// # use sqlite_hashes::rusqlite::{Connection, Result}; 12 | /// # use sqlite_hashes::register_fnv_functions; 13 | /// # fn main() -> Result<()> { 14 | /// let db = Connection::open_in_memory()?; 15 | /// register_fnv_functions(&db)?; 16 | /// let hash: Vec = db.query_row("SELECT fnv1a('hello')", [], |r| r.get(0))?; 17 | /// let expected = b"\xA4\x30\xD8\x46\x80\xAA\xBD\x0B"; 18 | /// assert_eq!(hash, expected); 19 | /// # Ok(()) 20 | /// # } 21 | /// ``` 22 | pub fn register_fnv_functions(conn: &Connection) -> Result<()> { 23 | crate::scalar::create_hash_fn::(conn, "fnv1a") 24 | } 25 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(feature = "default", doc = include_str!("../README.md"))] 2 | // 3 | // Unsafe code is required for cdylib, so only use it for this crate 4 | #![forbid(unsafe_code)] 5 | 6 | #[cfg(not(any( 7 | feature = "md5", 8 | feature = "sha1", 9 | feature = "sha224", 10 | feature = "sha256", 11 | feature = "sha384", 12 | feature = "sha512", 13 | feature = "blake3", 14 | feature = "fnv", 15 | feature = "xxhash", 16 | )))] 17 | compile_error!( 18 | "At least one of these features must be enabled: md5,sha1,sha224,sha256,sha384,sha512,blake3,fnv,xxhash" 19 | ); 20 | 21 | /// Re-export of the [`rusqlite`](https://crates.io/crates/rusqlite) crate to avoid version conflicts. 22 | pub use rusqlite; 23 | 24 | use crate::rusqlite::{Connection, Result}; 25 | 26 | mod aggregate; 27 | 28 | mod scalar; 29 | pub use crate::scalar::NamedDigest; 30 | 31 | mod state; 32 | pub use crate::state::HashState; 33 | 34 | #[cfg(feature = "md5")] 35 | mod md5; 36 | 37 | #[cfg(feature = "md5")] 38 | pub use crate::md5::register_md5_functions; 39 | 40 | #[cfg(feature = "sha1")] 41 | mod sha1; 42 | 43 | #[cfg(feature = "sha1")] 44 | pub use crate::sha1::register_sha1_functions; 45 | 46 | #[cfg(feature = "sha224")] 47 | mod sha224; 48 | 49 | #[cfg(feature = "sha224")] 50 | pub use crate::sha224::register_sha224_functions; 51 | 52 | #[cfg(feature = "sha256")] 53 | mod sha256; 54 | 55 | #[cfg(feature = "sha256")] 56 | pub use crate::sha256::register_sha256_functions; 57 | 58 | #[cfg(feature = "sha384")] 59 | mod sha384; 60 | 61 | #[cfg(feature = "sha384")] 62 | pub use crate::sha384::register_sha384_functions; 63 | 64 | #[cfg(feature = "sha512")] 65 | mod sha512; 66 | 67 | #[cfg(feature = "sha512")] 68 | pub use crate::sha512::register_sha512_functions; 69 | 70 | #[cfg(feature = "blake3")] 71 | mod blake3; 72 | 73 | #[cfg(feature = "blake3")] 74 | pub use crate::blake3::register_blake3_functions; 75 | 76 | #[cfg(feature = "fnv")] 77 | mod fnv; 78 | 79 | #[cfg(feature = "fnv")] 80 | pub use crate::fnv::register_fnv_functions; 81 | 82 | #[cfg(feature = "xxhash")] 83 | mod xxhash; 84 | 85 | #[cfg(feature = "xxhash")] 86 | pub use crate::xxhash::register_xxhash_functions; 87 | 88 | /// Register all hashing functions for the given `SQLite` connection. 89 | /// This is a convenience function that calls all of the `register_*_function` functions. 90 | /// Features must be enabled for the corresponding functions to be registered. 91 | /// 92 | /// # Example 93 | /// 94 | /// ``` 95 | /// # use sqlite_hashes::rusqlite::{Connection, Result}; 96 | /// # use sqlite_hashes::register_hash_functions; 97 | /// # fn main() -> Result<()> { 98 | /// let db = Connection::open_in_memory()?; 99 | /// register_hash_functions(&db)?; 100 | /// # if cfg!(all(feature = "hex", feature = "md5")) { 101 | /// let hash: String = db.query_row("SELECT md5_hex('hello')", [], |r| r.get(0))?; 102 | /// assert_eq!(&hash, "5D41402ABC4B2A76B9719D911017C592"); 103 | /// # } 104 | /// # if cfg!(all(feature = "hex", feature = "sha1")) { 105 | /// let hash: String = db.query_row("SELECT sha1_hex('hello')", [], |r| r.get(0))?; 106 | /// assert_eq!(hash, "AAF4C61DDCC5E8A2DABEDE0F3B482CD9AEA9434D"); 107 | /// # } 108 | /// # if cfg!(all(feature = "hex", feature = "sha224")) { 109 | /// let hash: String = db.query_row("SELECT sha224_hex('hello')", [], |r| r.get(0))?; 110 | /// assert_eq!(&hash, "EA09AE9CC6768C50FCEE903ED054556E5BFC8347907F12598AA24193"); 111 | /// # } 112 | /// # if cfg!(all(feature = "hex", feature = "sha256")) { 113 | /// let hash: String = db.query_row("SELECT sha256_hex('hello')", [], |r| r.get(0))?; 114 | /// assert_eq!(&hash, "2CF24DBA5FB0A30E26E83B2AC5B9E29E1B161E5C1FA7425E73043362938B9824"); 115 | /// # } 116 | /// # if cfg!(all(feature = "hex", feature = "sha384")) { 117 | /// let hash: String = db.query_row("SELECT sha384_hex('hello')", [], |r| r.get(0))?; 118 | /// assert_eq!(&hash, "59E1748777448C69DE6B800D7A33BBFB9FF1B463E44354C3553BCDB9C666FA90125A3C79F90397BDF5F6A13DE828684F"); 119 | /// # } 120 | /// # if cfg!(all(feature = "hex", feature = "sha512")) { 121 | /// let hash: String = db.query_row("SELECT sha512_hex('hello')", [], |r| r.get(0))?; 122 | /// assert_eq!(hash, "9B71D224BD62F3785D96D46AD3EA3D73319BFBC2890CAADAE2DFF72519673CA72323C3D99BA5C11D7C7ACC6E14B8C5DA0C4663475C2E5C3ADEF46F73BCDEC043"); 123 | /// # } 124 | /// # if cfg!(all(feature = "hex", feature = "blake3")) { 125 | /// let hash: String = db.query_row("SELECT blake3_hex('hello')", [], |r| r.get(0))?; 126 | /// assert_eq!(hash, "EA8F163DB38682925E4491C5E58D4BB3506EF8C14EB78A86E908C5624A67200F"); 127 | /// # } 128 | /// # if cfg!(all(feature = "hex", feature = "fnv")) { 129 | /// let hash: String = db.query_row("SELECT fnv1a_hex('hello')", [], |r| r.get(0))?; 130 | /// assert_eq!(hash, "A430D84680AABD0B"); 131 | /// # } 132 | /// # if cfg!(all(feature = "hex", feature = "xxhash")) { 133 | /// let hash: String = db.query_row("SELECT xxh32_hex('hello')", [], |r| r.get(0))?; 134 | /// assert_eq!(hash, "FB0077F9"); 135 | /// let hash: String = db.query_row("SELECT xxh64_hex('hello')", [], |r| r.get(0))?; 136 | /// assert_eq!(hash, "26C7827D889F6DA3"); 137 | /// let hash: String = db.query_row("SELECT xxh3_64_hex('hello')", [], |r| r.get(0))?; 138 | /// assert_eq!(hash, "9555E8555C62DCFD"); 139 | /// let hash: String = db.query_row("SELECT xxh3_128_hex('hello')", [], |r| r.get(0))?; 140 | /// assert_eq!(hash, "B5E9C1AD071B3E7FC779CFAA5E523818"); 141 | /// # } 142 | /// # Ok(()) 143 | /// # } 144 | /// ``` 145 | pub fn register_hash_functions(conn: &Connection) -> Result<()> { 146 | #[cfg(feature = "md5")] 147 | register_md5_functions(conn)?; 148 | #[cfg(feature = "sha1")] 149 | register_sha1_functions(conn)?; 150 | #[cfg(feature = "sha224")] 151 | register_sha224_functions(conn)?; 152 | #[cfg(feature = "sha256")] 153 | register_sha256_functions(conn)?; 154 | #[cfg(feature = "sha384")] 155 | register_sha384_functions(conn)?; 156 | #[cfg(feature = "sha512")] 157 | register_sha512_functions(conn)?; 158 | #[cfg(feature = "blake3")] 159 | register_blake3_functions(conn)?; 160 | #[cfg(feature = "fnv")] 161 | register_fnv_functions(conn)?; 162 | #[cfg(feature = "xxhash")] 163 | register_xxhash_functions(conn)?; 164 | 165 | Ok(()) 166 | } 167 | -------------------------------------------------------------------------------- /src/md5.rs: -------------------------------------------------------------------------------- 1 | use md5::Md5; 2 | 3 | use crate::rusqlite::{Connection, Result}; 4 | use crate::scalar::create_hash_fn; 5 | 6 | /// Register the `md5` SQL function with the given `SQLite` connection. 7 | /// The function takes a single argument and returns the [MD5 hash](https://en.wikipedia.org/wiki/MD5) (blob) of that argument. 8 | /// The argument can be either a string or a blob. 9 | /// If the argument is `NULL`, the result is `NULL`. 10 | /// 11 | /// # Example 12 | /// 13 | /// ``` 14 | /// # use sqlite_hashes::rusqlite::{Connection, Result}; 15 | /// # use sqlite_hashes::register_md5_functions; 16 | /// # fn main() -> Result<()> { 17 | /// let db = Connection::open_in_memory()?; 18 | /// register_md5_functions(&db)?; 19 | /// let hash: Vec = db.query_row("SELECT md5('hello')", [], |r| r.get(0))?; 20 | /// let expected = b"\x5d\x41\x40\x2a\xbc\x4b\x2a\x76\xb9\x71\x9d\x91\x10\x17\xc5\x92"; 21 | /// assert_eq!(hash, expected); 22 | /// # Ok(()) 23 | /// # } 24 | /// ``` 25 | pub fn register_md5_functions(conn: &Connection) -> Result<()> { 26 | create_hash_fn::(conn, "md5") 27 | } 28 | -------------------------------------------------------------------------------- /src/scalar.rs: -------------------------------------------------------------------------------- 1 | use std::panic::{RefUnwindSafe, UnwindSafe}; 2 | 3 | use digest::Digest; 4 | #[cfg(feature = "trace")] 5 | use log::trace; 6 | use rusqlite::functions::Context; 7 | use rusqlite::ToSql; 8 | 9 | #[cfg(feature = "aggregate")] 10 | use crate::aggregate::create_agg_function; 11 | use crate::rusqlite::functions::FunctionFlags; 12 | use crate::rusqlite::types::{Type, ValueRef}; 13 | use crate::rusqlite::Error::{InvalidFunctionParameterType, InvalidParameterCount}; 14 | use crate::rusqlite::{Connection, Result}; 15 | use crate::state::HashState; 16 | 17 | #[cfg(not(feature = "trace"))] 18 | macro_rules! trace { 19 | ($($arg:tt)*) => {}; 20 | } 21 | 22 | pub trait NamedDigest: Digest { 23 | fn name() -> &'static str; 24 | } 25 | 26 | macro_rules! digest_names { 27 | ($($typ:ty => $name:literal),* $(,)?) => { 28 | digest_names!( 29 | $( 30 | $typ => $name @ $name, 31 | )* 32 | ); 33 | }; 34 | ($($typ:ty => $name:literal @ $feature:literal),* $(,)?) => { 35 | $( 36 | #[cfg(feature = $feature)] 37 | impl NamedDigest for $typ { 38 | fn name() -> &'static str { 39 | $name 40 | } 41 | } 42 | )* 43 | }; 44 | } 45 | 46 | digest_names! { 47 | md5::Md5 => "md5", 48 | sha1::Sha1 => "sha1", 49 | sha2::Sha224 => "sha224", 50 | sha2::Sha256 => "sha256", 51 | sha2::Sha384 => "sha384", 52 | sha2::Sha512 => "sha512", 53 | blake3::Hasher => "blake3", 54 | } 55 | 56 | // Explicitly specify the feature flags when the fn name is different 57 | digest_names! { 58 | noncrypto_digests::Fnv => "fnv1a" @ "fnv", 59 | noncrypto_digests::Xxh32 => "xxh32" @ "xxhash", 60 | noncrypto_digests::Xxh64 => "xxh64" @ "xxhash", 61 | noncrypto_digests::Xxh3_64 => "xxh3_64" @ "xxhash", 62 | noncrypto_digests::Xxh3_128 => "xxh3_128" @ "xxhash", 63 | } 64 | 65 | pub(crate) fn create_hash_fn( 66 | conn: &Connection, 67 | fn_name: &str, 68 | ) -> Result<()> { 69 | create_scalar_function(conn, fn_name, |c| { 70 | hash_fn::( 71 | c, 72 | #[cfg(feature = "trace")] 73 | "", 74 | ) 75 | .map(HashState::finalize) 76 | })?; 77 | 78 | #[cfg(feature = "hex")] 79 | { 80 | let fn_name = format!("{fn_name}_hex"); 81 | create_scalar_function(conn, &fn_name, |c| { 82 | hash_fn::( 83 | c, 84 | #[cfg(feature = "trace")] 85 | "_hex", 86 | ) 87 | .map(HashState::finalize_hex) 88 | })?; 89 | } 90 | 91 | #[cfg(feature = "aggregate")] 92 | { 93 | let fn_name = format!("{fn_name}_concat"); 94 | create_agg_function( 95 | conn, 96 | &fn_name, 97 | crate::aggregate::AggType::>::new( 98 | #[cfg(feature = "trace")] 99 | &fn_name, 100 | HashState::finalize, 101 | ), 102 | )?; 103 | } 104 | 105 | #[cfg(all(feature = "aggregate", feature = "hex"))] 106 | { 107 | let fn_name = format!("{fn_name}_concat_hex"); 108 | create_agg_function( 109 | conn, 110 | &fn_name, 111 | crate::aggregate::AggType::::new( 112 | #[cfg(feature = "trace")] 113 | &fn_name, 114 | HashState::finalize_hex, 115 | ), 116 | )?; 117 | } 118 | 119 | Ok(()) 120 | } 121 | 122 | pub fn create_scalar_function(conn: &Connection, fn_name: &str, function: F) -> Result<()> 123 | where 124 | // TODO: Newer versions do not require UnwindSafe 125 | F: Fn(&Context<'_>) -> Result + Send + UnwindSafe + 'static, 126 | T: ToSql, 127 | { 128 | trace!("Registering function {fn_name}"); 129 | conn.create_scalar_function( 130 | fn_name, 131 | -1, 132 | FunctionFlags::SQLITE_UTF8 133 | | FunctionFlags::SQLITE_DETERMINISTIC 134 | | FunctionFlags::SQLITE_DIRECTONLY, 135 | function, 136 | ) 137 | } 138 | 139 | fn hash_fn( 140 | ctx: &Context, 141 | #[cfg(feature = "trace")] suffix: &'static str, 142 | ) -> Result> { 143 | let param_count = ctx.len(); 144 | if param_count == 0 { 145 | return Err(InvalidParameterCount(param_count, 1)); 146 | } 147 | let mut state = HashState::::default(); 148 | for idx in 0..param_count { 149 | let value = ctx.get_raw(idx); 150 | match value { 151 | ValueRef::Blob(val) => { 152 | trace!("{}{suffix}: hashing blob arg{idx}={val:?}", T::name()); 153 | state.add_value(val); 154 | } 155 | ValueRef::Text(val) => { 156 | trace!("{}{suffix}: hashing text arg{idx}={val:?}", T::name()); 157 | state.add_value(val); 158 | } 159 | ValueRef::Null => { 160 | trace!("{}{suffix}: ignoring arg{idx}=NULL", T::name()); 161 | state.add_null(); 162 | } 163 | #[allow(unused_variables)] 164 | ValueRef::Integer(val) => { 165 | trace!( 166 | "{}{suffix}: unsupported Integer arg{idx}={val:?}", 167 | T::name() 168 | ); 169 | Err(InvalidFunctionParameterType(0, Type::Integer))?; 170 | } 171 | #[allow(unused_variables)] 172 | ValueRef::Real(val) => { 173 | trace!("{}{suffix}: unsupported Real arg{idx}={val:?}", T::name()); 174 | Err(InvalidFunctionParameterType(0, Type::Real))?; 175 | } 176 | } 177 | } 178 | 179 | Ok(state) 180 | } 181 | -------------------------------------------------------------------------------- /src/sha1.rs: -------------------------------------------------------------------------------- 1 | use sha1::Sha1; 2 | 3 | use crate::rusqlite::{Connection, Result}; 4 | 5 | /// Register the `sha1` SQL function with the given `SQLite` connection. 6 | /// The function takes a single argument and returns the [SHA1 hash](https://en.wikipedia.org/wiki/SHA-1) (blob) of that argument. 7 | /// The argument can be either a string or a blob. 8 | /// If the argument is `NULL`, the result is `NULL`. 9 | /// 10 | /// # Example 11 | /// 12 | /// ``` 13 | /// # use sqlite_hashes::rusqlite::{Connection, Result}; 14 | /// # use sqlite_hashes::register_sha1_functions; 15 | /// # fn main() -> Result<()> { 16 | /// let db = Connection::open_in_memory()?; 17 | /// register_sha1_functions(&db)?; 18 | /// let hash: Vec = db.query_row("SELECT sha1('hello')", [], |r| r.get(0))?; 19 | /// let expected = b"\xaa\xf4\xc6\x1d\xdc\xc5\xe8\xa2\xda\xbe\xde\x0f\x3b\x48\x2c\xd9\xae\xa9\x43\x4d"; 20 | /// assert_eq!(hash, expected); 21 | /// # Ok(()) 22 | /// # } 23 | /// ``` 24 | pub fn register_sha1_functions(conn: &Connection) -> Result<()> { 25 | crate::scalar::create_hash_fn::(conn, "sha1") 26 | } 27 | -------------------------------------------------------------------------------- /src/sha224.rs: -------------------------------------------------------------------------------- 1 | use sha2::Sha224; 2 | 3 | use crate::rusqlite::{Connection, Result}; 4 | 5 | /// Register the `sha224` SQL function with the given `SQLite` connection. 6 | /// The function takes a single argument and returns the [SHA224 hash](https://en.wikipedia.org/wiki/SHA-2) (blob) of that argument. 7 | /// The argument can be either a string or a blob. 8 | /// If the argument is `NULL`, the result is `NULL`. 9 | /// 10 | /// # Example 11 | /// 12 | /// ``` 13 | /// # use sqlite_hashes::rusqlite::{Connection, Result}; 14 | /// # use sqlite_hashes::register_sha224_functions; 15 | /// # fn main() -> Result<()> { 16 | /// let db = Connection::open_in_memory()?; 17 | /// register_sha224_functions(&db)?; 18 | /// let hash: Vec = db.query_row("SELECT sha224('hello')", [], |r| r.get(0))?; 19 | /// let expected = b"\xea\x09\xae\x9c\xc6\x76\x8c\x50\xfc\xee\x90\x3e\xd0\x54\x55\x6e\x5b\xfc\x83\x47\x90\x7f\x12\x59\x8a\xa2\x41\x93"; 20 | /// assert_eq!(hash, expected); 21 | /// # Ok(()) 22 | /// # } 23 | /// ``` 24 | pub fn register_sha224_functions(conn: &Connection) -> Result<()> { 25 | crate::scalar::create_hash_fn::(conn, "sha224") 26 | } 27 | -------------------------------------------------------------------------------- /src/sha256.rs: -------------------------------------------------------------------------------- 1 | use sha2::Sha256; 2 | 3 | use crate::rusqlite::{Connection, Result}; 4 | 5 | /// Register the `sha256` SQL function with the given `SQLite` connection. 6 | /// The function takes a single argument and returns the [SHA256 hash](https://en.wikipedia.org/wiki/SHA-2) (blob) of that argument. 7 | /// The argument can be either a string or a blob. 8 | /// If the argument is `NULL`, the result is `NULL`. 9 | /// 10 | /// # Example 11 | /// 12 | /// ``` 13 | /// # use sqlite_hashes::rusqlite::{Connection, Result}; 14 | /// # use sqlite_hashes::register_sha256_functions; 15 | /// # fn main() -> Result<()> { 16 | /// let db = Connection::open_in_memory()?; 17 | /// register_sha256_functions(&db)?; 18 | /// let hash: Vec = db.query_row("SELECT sha256('hello')", [], |r| r.get(0))?; 19 | /// let expected = b"\x2c\xf2\x4d\xba\x5f\xb0\xa3\x0e\x26\xe8\x3b\x2a\xc5\xb9\xe2\x9e\x1b\x16\x1e\x5c\x1f\xa7\x42\x5e\x73\x04\x33\x62\x93\x8b\x98\x24"; 20 | /// assert_eq!(hash, expected); 21 | /// # Ok(()) 22 | /// # } 23 | /// ``` 24 | pub fn register_sha256_functions(conn: &Connection) -> Result<()> { 25 | crate::scalar::create_hash_fn::(conn, "sha256") 26 | } 27 | -------------------------------------------------------------------------------- /src/sha384.rs: -------------------------------------------------------------------------------- 1 | use sha2::Sha384; 2 | 3 | use crate::rusqlite::{Connection, Result}; 4 | 5 | /// Register the `sha384` SQL function with the given `SQLite` connection. 6 | /// The function takes a single argument and returns the [SHA384 hash](https://en.wikipedia.org/wiki/SHA-2) (blob) of that argument. 7 | /// The argument can be either a string or a blob. 8 | /// If the argument is `NULL`, the result is `NULL`. 9 | /// 10 | /// # Example 11 | /// 12 | /// ``` 13 | /// # use sqlite_hashes::rusqlite::{Connection, Result}; 14 | /// # use sqlite_hashes::register_sha384_functions; 15 | /// # fn main() -> Result<()> { 16 | /// let db = Connection::open_in_memory()?; 17 | /// register_sha384_functions(&db)?; 18 | /// let hash: Vec = db.query_row("SELECT sha384('hello')", [], |r| r.get(0))?; 19 | /// let expected = b"\x59\xe1\x74\x87\x77\x44\x8c\x69\xde\x6b\x80\x0d\x7a\x33\xbb\xfb\x9f\xf1\xb4\x63\xe4\x43\x54\xc3\x55\x3b\xcd\xb9\xc6\x66\xfa\x90\x12\x5a\x3c\x79\xf9\x03\x97\xbd\xf5\xf6\xa1\x3d\xe8\x28\x68\x4f"; 20 | /// assert_eq!(hash, expected); 21 | /// # Ok(()) 22 | /// # } 23 | /// ``` 24 | pub fn register_sha384_functions(conn: &Connection) -> Result<()> { 25 | crate::scalar::create_hash_fn::(conn, "sha384") 26 | } 27 | -------------------------------------------------------------------------------- /src/sha512.rs: -------------------------------------------------------------------------------- 1 | use sha2::Sha512; 2 | 3 | use crate::rusqlite::{Connection, Result}; 4 | 5 | /// Register the `sha512` SQL function with the given `SQLite` connection. 6 | /// The function takes a single argument and returns the [SHA512 hash](https://en.wikipedia.org/wiki/SHA-2) (blob) of that argument. 7 | /// The argument can be either a string or a blob. 8 | /// If the argument is `NULL`, the result is `NULL`. 9 | /// 10 | /// # Example 11 | /// 12 | /// ``` 13 | /// # use sqlite_hashes::rusqlite::{Connection, Result}; 14 | /// # use sqlite_hashes::register_sha512_functions; 15 | /// # fn main() -> Result<()> { 16 | /// let db = Connection::open_in_memory()?; 17 | /// register_sha512_functions(&db)?; 18 | /// let hash: Vec = db.query_row("SELECT sha512('hello')", [], |r| r.get(0))?; 19 | /// let expected = b"\x9b\x71\xd2\x24\xbd\x62\xf3\x78\x5d\x96\xd4\x6a\xd3\xea\x3d\x73\x31\x9b\xfb\xc2\x89\x0c\xaa\xda\xe2\xdf\xf7\x25\x19\x67\x3c\xa7\x23\x23\xc3\xd9\x9b\xa5\xc1\x1d\x7c\x7a\xcc\x6e\x14\xb8\xc5\xda\x0c\x46\x63\x47\x5c\x2e\x5c\x3a\xde\xf4\x6f\x73\xbc\xde\xc0\x43"; 20 | /// assert_eq!(hash, expected); 21 | /// # Ok(()) 22 | /// # } 23 | /// ``` 24 | pub fn register_sha512_functions(conn: &Connection) -> Result<()> { 25 | crate::scalar::create_hash_fn::(conn, "sha512") 26 | } 27 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | use digest::Digest; 2 | #[cfg(feature = "hex")] 3 | use hex::ToHex as _; 4 | 5 | #[derive(Debug, Default)] 6 | pub enum HashState { 7 | #[default] 8 | Created, 9 | Started, 10 | HasValues(T), 11 | } 12 | 13 | impl HashState { 14 | #[inline] 15 | pub fn add_null(&mut self) { 16 | if let Self::Created = self { 17 | *self = Self::Started; 18 | } 19 | } 20 | 21 | #[inline] 22 | pub fn add_value(&mut self, value: &[u8]) { 23 | match self { 24 | Self::Created | Self::Started => { 25 | let mut hasher = T::new(); 26 | hasher.update(value); 27 | *self = Self::HasValues(hasher); 28 | } 29 | Self::HasValues(hasher) => { 30 | hasher.update(value); 31 | } 32 | } 33 | } 34 | 35 | #[inline] 36 | pub fn finalize(self) -> Option> { 37 | match self { 38 | Self::Created | Self::Started => None, 39 | Self::HasValues(hasher) => Some(hasher.finalize().to_vec()), 40 | } 41 | } 42 | 43 | #[inline] 44 | #[cfg(feature = "hex")] 45 | pub fn finalize_hex(self) -> Option { 46 | match self { 47 | Self::Created => None, 48 | Self::Started => Some(String::new()), 49 | Self::HasValues(hasher) => Some(hasher.finalize().to_vec().encode_hex_upper()), 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/xxhash.rs: -------------------------------------------------------------------------------- 1 | use noncrypto_digests::{Xxh32, Xxh3_128, Xxh3_64, Xxh64}; 2 | 3 | use crate::rusqlite::{Connection, Result}; 4 | 5 | /// Register `xxh32`, `xxh64`, `xxh3_64`, `xxh3_128`, `xxh3_64` SQL functions with the given `SQLite` connection. 6 | /// The functions use [Rust xxHash implementation](https://github.com/DoumanAsh/xxhash-rust) to compute the hash of the argument(s) using zero as the seed value. 7 | /// 8 | /// # Example 9 | /// 10 | /// ``` 11 | /// # // Use Python to convert: 12 | /// # // print('"\\x' + '\\x'.join([f"{v:02X}" for v in [251, 0, 119, 249]])+'"') 13 | /// # use sqlite_hashes::rusqlite::{Connection, Result}; 14 | /// # use sqlite_hashes::register_xxhash_functions; 15 | /// # fn main() -> Result<()> { 16 | /// let db = Connection::open_in_memory()?; 17 | /// register_xxhash_functions(&db)?; 18 | /// let hash: Vec = db.query_row("SELECT xxh32('hello')", [], |r| r.get(0))?; 19 | /// let expected = b"\xFB\x00\x77\xF9"; 20 | /// assert_eq!(hash, expected); 21 | /// let hash: Vec = db.query_row("SELECT xxh64('hello')", [], |r| r.get(0))?; 22 | /// let expected = b"\x26\xC7\x82\x7D\x88\x9F\x6D\xA3"; 23 | /// assert_eq!(hash, expected); 24 | /// let hash: Vec = db.query_row("SELECT xxh3_64('hello')", [], |r| r.get(0))?; 25 | /// let expected = b"\x95\x55\xE8\x55\x5C\x62\xDC\xFD"; 26 | /// assert_eq!(hash, expected); 27 | /// let hash: Vec = db.query_row("SELECT xxh3_128('hello')", [], |r| r.get(0))?; 28 | /// let expected = b"\xb5\xe9\xc1\xad\x07\x1b\x3e\x7f\xc7\x79\xcf\xaa\x5e\x52\x38\x18"; 29 | /// assert_eq!(hash, expected); 30 | /// # Ok(()) 31 | /// # } 32 | /// ``` 33 | pub fn register_xxhash_functions(conn: &Connection) -> Result<()> { 34 | crate::scalar::create_hash_fn::(conn, "xxh32")?; 35 | crate::scalar::create_hash_fn::(conn, "xxh64")?; 36 | crate::scalar::create_hash_fn::(conn, "xxh3_64")?; 37 | crate::scalar::create_hash_fn::(conn, "xxh3_128") 38 | } 39 | -------------------------------------------------------------------------------- /tests/_utils.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code, unused_macros)] 2 | #![allow( 3 | clippy::missing_panics_doc, 4 | clippy::must_use_candidate, 5 | clippy::new_without_default 6 | )] 7 | 8 | use std::fmt::Write as _; 9 | 10 | use digest::Digest; 11 | use insta::assert_snapshot; 12 | use rusqlite::types::FromSql; 13 | use rusqlite::{Connection, Result}; 14 | 15 | /// Simple hasher function that returns the hex-encoded hash of the input. 16 | #[must_use] 17 | pub fn hash(input: &[u8]) -> Vec { 18 | let mut hasher = T::new(); 19 | hasher.update(input); 20 | hasher.finalize().to_vec() 21 | } 22 | 23 | /// Simple hasher function that returns the hex-encoded hash of the input. 24 | #[must_use] 25 | pub fn hash_hex(input: &[u8]) -> String { 26 | // Even though hex crate provides this functionality, its use is optional, 27 | // so we do it manually here to avoid test dependency on hex. 28 | hash::(input) 29 | .into_iter() 30 | .fold(String::new(), |mut output, b| { 31 | let _ = write!(output, "{b:02X}"); 32 | output 33 | }) 34 | } 35 | 36 | /// Make sure the above hasher function produces the expected values, 37 | /// and use it for validating the SQL functions. 38 | fn hasher() { 39 | #[cfg(feature = "md5")] 40 | assert_snapshot!(hash_hex::("test".as_bytes()), @"098F6BCD4621D373CADE4E832627B4F6"); 41 | #[cfg(feature = "sha1")] 42 | assert_snapshot!(hash_hex::("test".as_bytes()), @"A94A8FE5CCB19BA61C4C0873D391E987982FBBD3"); 43 | #[cfg(feature = "sha224")] 44 | assert_snapshot!(hash_hex::("test".as_bytes()), @"90A3ED9E32B2AAF4C61C410EB925426119E1A9DC53D4286ADE99A809"); 45 | #[cfg(feature = "sha256")] 46 | assert_snapshot!(hash_hex::("test".as_bytes()), @"9F86D081884C7D659A2FEAA0C55AD015A3BF4F1B2B0B822CD15D6C15B0F00A08"); 47 | #[cfg(feature = "sha384")] 48 | assert_snapshot!(hash_hex::("test".as_bytes()), @"768412320F7B0AA5812FCE428DC4706B3CAE50E02A64CAA16A782249BFE8EFC4B7EF1CCB126255D196047DFEDF17A0A9"); 49 | #[cfg(feature = "sha512")] 50 | assert_snapshot!(hash_hex::("test".as_bytes()), @"EE26B0DD4AF7E749AA1A8EE3C10AE9923F618980772E473F8819A5D4940E0DB27AC185F8A0E1D5F84F88BC887FD67B143732C304CC5FA9AD8E6F57F50028A8FF"); 51 | #[cfg(feature = "blake3")] 52 | assert_snapshot!(hash_hex::("test".as_bytes()), @"BA80A53F981C4D0D6A2797BEEA0D8B8A7A0B1E8B6A27E4F7A0E3C6C7E6F7A0E3C6C7E6F7A0E3C6C7E6F7A0E3C6C7E6F"); 53 | #[cfg(feature = "fnv")] 54 | assert_snapshot!(hash_hex::("test".as_bytes()), @"EE26B0DD4AF7E749AA1A8EE3C10AE9923F618980772E473F8819A5D4940E0DB27AC185F8A0E1D5F84F88BC887FD67B143732C304CC5FA9AD8E6F57F50028A8FF"); 55 | #[cfg(feature = "xxhash")] 56 | { 57 | assert_snapshot!(hash_hex::("test".as_bytes()), @"EE26B0DD4AF7E749AA1A8EE3C10AE9923F618980772E473F8819A5D4940E0DB27AC185F8A0E1D5F84F88BC887FD67B143732C304CC5FA9AD8E6F57F50028A8FF"); 58 | assert_snapshot!(hash_hex::("test".as_bytes()), @"EE26B0DD4AF7E749AA1A8EE3C10AE9923F618980772E473F8819A5D4940E0DB27AC185F8A0E1D5F84F88BC887FD67B143732C304CC5FA9AD8E6F57F50028A8FF"); 59 | assert_snapshot!(hash_hex::("test".as_bytes()), @"EE26B0DD4AF7E749AA1A8EE3C10AE9923F618980772E473F8819A5D4940E0DB27AC185F8A0E1D5F84F88BC887FD67B143732C304CC5FA9AD8E6F57F50028A8FF"); 60 | assert_snapshot!(hash_hex::("test".as_bytes()), @"EE26B0DD4AF7E749AA1A8EE3C10AE9923F618980772E473F8819A5D4940E0DB27AC185F8A0E1D5F84F88BC887FD67B143732C304CC5FA9AD8E6F57F50028A8FF"); 61 | } 62 | } 63 | 64 | /// Create macros like `md5!` asserting that first expression equals to the hash of the second one. 65 | /// The macro evaluates to nothing if the corresponding feature is disabled. 66 | /// The macro accepts the following syntax, comparing the `actual` value differently against 67 | /// - `md5!(actual, expected)` - expected string will be encoded as a hex string 68 | /// - `md5!(actual, blob(expected))` - expected string will be encoded as a byte array 69 | /// - `md5!(actual, bytes_as_blob(expected) )` - expected is a byte array to be encoded as a byte array 70 | /// - `md5!(actual, bytes_as_hex(expected) )` - expected is a byte array to be encoded as a hex string 71 | /// - `md5!(actual, [ expected ])` - expected is a vector of strings to be encoded as a hex strings 72 | /// - `md5!(actual, blob[ expected ])` - expected is a vector of strings to be encoded as a byte arrays 73 | /// - `md5!(actual, bytes_as_blob[ expected ])` - expected is a vector of byte arrays to be encoded as a byte arrays 74 | /// - `md5!(actual, bytes_as_hex[ expected ])` - expected is a vector of byte arrays to be encoded as a hex strings 75 | macro_rules! hash_macros { 76 | ( $( $feat:literal $name:ident $typ:ty ),* $(,)? ) => { 77 | $( 78 | #[cfg(feature = $feat)] 79 | macro_rules! $name { 80 | ( $actual:expr, NULL ) => {{ 81 | let actual: rusqlite::Result>> = $actual; 82 | assert_eq!(actual, Ok(None), "asserting NULL result"); 83 | }}; 84 | ( $actual:expr, EMPTY ) => {{ 85 | let actual: rusqlite::Result> = $actual; 86 | assert_eq!(actual, Ok(Some(String::from(""))), "asserting EMPTY result"); 87 | }}; 88 | ( $actual:expr, ERROR ) => {{ 89 | let actual: rusqlite::Result, rusqlite::Error> = $actual; 90 | assert!(actual.is_err(), "asserting error result"); 91 | }}; 92 | ( $actual:expr, NO_ROWS ) => {{ 93 | let actual: rusqlite::Result>> = $actual; 94 | assert!(actual.unwrap().is_empty(), "asserting NO_ROWS result"); 95 | }}; 96 | ( $actual:expr, blob[ $vec:expr ] ) => {{ 97 | let actual: rusqlite::Result>> = $actual; 98 | assert_eq!(actual.unwrap(), $vec.iter().map(|v| $crate::utils::hash::<$typ>(v.as_bytes())).collect::>>()) 99 | }}; 100 | ( $actual:expr, bytes_as_blob[ $vec:expr ] ) => {{ 101 | let actual: rusqlite::Result>> = $actual; 102 | assert_eq!(actual.unwrap(), $vec.iter().map(|v| $crate::utils::hash::<$typ>(v)).collect::>>()) 103 | }}; 104 | ( $actual:expr, bytes_as_hex[ $vec:expr ] ) => {{ 105 | let actual: rusqlite::Result> = $actual; 106 | assert_eq!(actual.unwrap(), $vec.iter().map(|v| $crate::utils::hash_hex::<$typ>(v)).collect::>>()) 107 | }}; 108 | ( $actual:expr, hex[ $vec:expr ] ) => {{ 109 | let actual: rusqlite::Result> = $actual; 110 | assert_eq!(actual.unwrap(), $vec.iter().map(|v| $crate::utils::hash_hex::<$typ>(v.as_bytes())).collect::>()) 111 | }}; 112 | ( $actual:expr, blob($expected:expr) ) => {{ 113 | let actual: rusqlite::Result> = $actual; 114 | assert_eq!(actual.unwrap(), $crate::utils::hash::<$typ>($expected.as_bytes())) 115 | }}; 116 | ( $actual:expr, bytes_as_blob($expected:expr) ) => {{ 117 | let actual: rusqlite::Result> = $actual; 118 | assert_eq!(actual.unwrap(), $crate::utils::hash::<$typ>($expected)) 119 | }}; 120 | ( $actual:expr, bytes_as_hex($expected:expr) ) => {{ 121 | let actual: rusqlite::Result = $actual; 122 | assert_eq!(actual.unwrap(), $crate::utils::hash_hex::<$typ>($expected)) 123 | }}; 124 | ( $actual:expr, hex($expected:expr) ) => {{ 125 | let actual: rusqlite::Result = $actual; 126 | assert_eq!(actual.unwrap(), $crate::utils::hash_hex::<$typ>($expected.as_bytes())) 127 | }}; 128 | } 129 | #[cfg(not(feature = $feat))] 130 | macro_rules! $name { 131 | ($actual:expr, $exp:expr ) => {}; 132 | } 133 | )* 134 | }; 135 | } 136 | 137 | hash_macros!( 138 | "md5" md5 md5::Md5, 139 | "sha1" sha1 sha1::Sha1, 140 | "sha224" sha224 sha2::Sha224, 141 | "sha256" sha256 sha2::Sha256, 142 | "sha384" sha384 sha2::Sha384, 143 | "sha512" sha512 sha2::Sha512, 144 | "blake3" blake3 blake3::Hasher, 145 | "fnv" fnv1a noncrypto_digests::Fnv, 146 | "xxhash" xxh32 noncrypto_digests::Xxh32, 147 | "xxhash" xxh64 noncrypto_digests::Xxh64, 148 | "xxhash" xxh3_64 noncrypto_digests::Xxh3_64, 149 | "xxhash" xxh3_128 noncrypto_digests::Xxh3_128, 150 | ); 151 | 152 | macro_rules! test_all { 153 | ( $conn:ident.$func:ident(*$suffix:tt), $($any:tt)* ) => {{ 154 | let suffix = stringify!($suffix); 155 | test_all!( $conn.$func(suffix), $($any)* ) 156 | }}; 157 | ( $conn:ident.$func:ident($suffix:expr), $($any:tt)* ) => {{ 158 | let suffix = $suffix; 159 | md5!( $conn.$func(&format!("md5{suffix}")), $($any)* ); 160 | sha1!( $conn.$func(&format!("sha1{suffix}")), $($any)* ); 161 | sha224!( $conn.$func(&format!("sha224{suffix}")), $($any)* ); 162 | sha256!( $conn.$func(&format!("sha256{suffix}")), $($any)* ); 163 | sha384!( $conn.$func(&format!("sha384{suffix}")), $($any)* ); 164 | sha512!( $conn.$func(&format!("sha512{suffix}")), $($any)* ); 165 | blake3!( $conn.$func(&format!("blake3{suffix}")), $($any)* ); 166 | fnv1a!( $conn.$func(&format!("fnv1a{suffix}")), $($any)* ); 167 | xxh32!( $conn.$func(&format!("xxh32{suffix}")), $($any)* ); 168 | xxh64!( $conn.$func(&format!("xxh64{suffix}")), $($any)* ); 169 | xxh3_64!( $conn.$func(&format!("xxh3_64{suffix}")), $($any)* ); 170 | xxh3_128!( $conn.$func(&format!("xxh3_128{suffix}")), $($any)* ); 171 | }}; 172 | } 173 | 174 | pub struct Conn(Connection); 175 | 176 | impl Conn { 177 | pub fn new() -> Self { 178 | let db = Connection::open_in_memory().unwrap(); 179 | sqlite_hashes::register_hash_functions(&db).unwrap(); 180 | db.execute_batch( 181 | " 182 | CREATE TABLE tbl(id INTEGER PRIMARY KEY, v_text TEXT, v_blob BLOB, v_null_text TEXT, v_null_blob BLOB); 183 | INSERT INTO tbl VALUES 184 | (1, 'bbb', cast('bbb' as BLOB), cast(NULL as TEXT), cast(NULL as BLOB)), 185 | (2, 'ccc', cast('ccc' as BLOB), cast(NULL as TEXT), cast(NULL as BLOB)), 186 | (3, 'aaa', cast('aaa' as BLOB), cast(NULL as TEXT), cast(NULL as BLOB)); 187 | ", 188 | ) 189 | .unwrap(); 190 | Self(db) 191 | } 192 | 193 | pub fn sql(&self, query: &str) -> Result { 194 | self.0.query_row_and_then(query, [], |r| r.get(0)) 195 | } 196 | 197 | pub fn list(&self, query: &str) -> Result> { 198 | let mut stmt = self.0.prepare(query).unwrap(); 199 | stmt.query_map([], |row| row.get::<_, T>(0)) 200 | .unwrap() 201 | .collect::>>() 202 | } 203 | 204 | pub fn select(&self, func: &str) -> Result { 205 | self.sql(&format!("SELECT {func}")) 206 | } 207 | 208 | /// Should return hashes of `[aaa, aaabbb, aaabbbccc]`. 209 | pub fn growing_text_seq(&self, func: &str) -> Result> { 210 | let sql = format!("SELECT {func}(v_text) OVER (ORDER BY v_text) FROM tbl"); 211 | self.list(&sql) 212 | } 213 | 214 | /// The ordering here is un-documented and may change in the future. 215 | pub fn legacy_text_aggregate(&self, hash: &str) -> Result { 216 | let sql = format!("SELECT {hash}(v_text) FROM (SELECT v_text FROM tbl ORDER BY v_text)"); 217 | self.sql(&sql) 218 | } 219 | 220 | /// The ordering here is un-documented and may change in the future. 221 | pub fn legacy_blob_aggregate(&self, hash: &str) -> Result { 222 | let sql = format!("SELECT {hash}(v_blob) FROM (SELECT v_blob FROM tbl ORDER BY v_text)"); 223 | self.sql(&sql) 224 | } 225 | 226 | /// The ordering here is un-documented and may change in the future. 227 | pub fn legacy_null_text_aggregate(&self, hash: &str) -> Result { 228 | let sql = format!( 229 | "SELECT {hash}(v_null_text) FROM (SELECT v_null_text FROM tbl ORDER BY v_text)" 230 | ); 231 | self.sql(&sql) 232 | } 233 | 234 | /// The ordering here is un-documented and may change in the future. 235 | pub fn legacy_null_blob_aggregate(&self, hash: &str) -> Result { 236 | let sql = format!( 237 | "SELECT {hash}(v_null_blob) FROM (SELECT v_null_blob FROM tbl ORDER BY v_text)" 238 | ); 239 | self.sql(&sql) 240 | } 241 | 242 | /// Use RECURSIVE CTE to generate a sequence of numbers from 1 to `iterations`, 243 | pub fn sequence(&self, expr: &str, iterations: usize) -> Result { 244 | // Modeled after https://stackoverflow.com/a/26241151/177275 245 | let sql = format!( 246 | " 247 | WITH RECURSIVE 248 | seq(v) AS ( 249 | SELECT 1 250 | UNION ALL 251 | SELECT v + 1 FROM seq 252 | LIMIT {iterations} 253 | ) 254 | SELECT {expr} FROM seq" 255 | ); 256 | self.sql(&sql) 257 | } 258 | 259 | pub fn seq_0(&self, expr: &str) -> Result { 260 | self.sequence(expr, 0) 261 | } 262 | 263 | pub fn seq_1(&self, expr: &str) -> Result { 264 | self.sequence(expr, 1) 265 | } 266 | 267 | pub fn seq_1000(&self, expr: &str) -> Result { 268 | self.sequence(expr, 1000) 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /tests/aggregate.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "aggregate")] 2 | 3 | #[macro_use] 4 | #[path = "_utils.rs"] 5 | mod utils; 6 | use std::fmt::Write as _; 7 | 8 | use crate::utils::Conn; 9 | 10 | #[ctor::ctor] 11 | fn init() { 12 | let _ = env_logger::builder().is_test(true).try_init(); 13 | } 14 | 15 | #[test] 16 | fn simple_concat() { 17 | let c = Conn::new(); 18 | test_all!(c.select("_concat(NULL)"), NULL); 19 | test_all!(c.select("_concat(NULL, NULL, NULL)"), NULL); 20 | test_all!(c.select("_concat(1)"), ERROR); 21 | test_all!(c.select("_concat(0.42)"), ERROR); 22 | test_all!(c.select("_concat()"), ERROR); 23 | test_all!(c.select("_concat('')"), blob("")); 24 | test_all!(c.select("_concat('a')"), blob("a")); 25 | test_all!(c.select("_concat('123456789')"), blob("123456789")); 26 | test_all!(c.select("_concat(x'')"), blob("")); 27 | test_all!(c.select("_concat(x'00')"), blob("\0")); 28 | test_all!( 29 | c.select("_concat(x'0123456789abcdef')"), 30 | bytes_as_blob(b"\x01\x23\x45\x67\x89\xab\xcd\xef") 31 | ); 32 | test_all!( 33 | c.select("_concat('', 'a', '123456789', x'', x'00', x'0123456789abcdef')"), 34 | bytes_as_blob(b"a123456789\x00\x01\x23\x45\x67\x89\xab\xcd\xef") 35 | ); 36 | test_all!( 37 | c.select( 38 | "_concat(NULL, 'a', NULL, '123456789', x'', x'00', NULL, x'0123456789abcdef', NULL)" 39 | ), 40 | bytes_as_blob(b"a123456789\x00\x01\x23\x45\x67\x89\xab\xcd\xef") 41 | ); 42 | } 43 | 44 | #[test] 45 | #[cfg(feature = "hex")] 46 | fn simple_concat_hex() { 47 | let c = Conn::new(); 48 | test_all!(c.select("_concat_hex(NULL)"), EMPTY); 49 | test_all!(c.select("_concat_hex(NULL, NULL, NULL)"), EMPTY); 50 | test_all!(c.select("_concat_hex(1)"), ERROR); 51 | test_all!(c.select("_concat_hex(0.42)"), ERROR); 52 | test_all!(c.select("_concat_hex()"), ERROR); 53 | test_all!(c.select("_concat_hex('')"), hex("")); 54 | test_all!(c.select("_concat_hex('a')"), hex("a")); 55 | test_all!(c.select("_concat_hex('123456789')"), hex("123456789")); 56 | test_all!(c.select("_concat_hex(x'')"), hex("")); 57 | test_all!(c.select("_concat_hex(x'00')"), hex("\0")); 58 | test_all!( 59 | c.select("_concat_hex(x'0123456789abcdef')"), 60 | bytes_as_hex(b"\x01\x23\x45\x67\x89\xab\xcd\xef") 61 | ); 62 | test_all!( 63 | c.select("_concat_hex('', 'a', '123456789', x'', x'00', x'0123456789abcdef')"), 64 | bytes_as_hex(b"a123456789\x00\x01\x23\x45\x67\x89\xab\xcd\xef") 65 | ); 66 | test_all!( 67 | c.select("_concat_hex(NULL, 'a', NULL, '123456789', x'', x'00', NULL, x'0123456789abcdef', NULL)"), 68 | bytes_as_hex(b"a123456789\x00\x01\x23\x45\x67\x89\xab\xcd\xef") 69 | ); 70 | } 71 | 72 | #[test] 73 | fn hash_concat() { 74 | let c = Conn::new(); 75 | test_all!(c.legacy_text_aggregate(*_concat), blob("aaabbbccc")); 76 | test_all!(c.legacy_blob_aggregate(*_concat), blob("aaabbbccc")); 77 | test_all!(c.legacy_null_text_aggregate(*_concat), NULL); 78 | test_all!(c.legacy_null_blob_aggregate(*_concat), NULL); 79 | } 80 | 81 | #[test] 82 | #[cfg(feature = "hex")] 83 | fn hash_concat_hex() { 84 | let c = Conn::new(); 85 | test_all!(c.legacy_text_aggregate(*_concat_hex), hex("aaabbbccc")); 86 | } 87 | 88 | #[test] 89 | fn concat_sequence() { 90 | let c = Conn::new(); 91 | 92 | test_all!(c.seq_0("_concat(cast(v as text))"), NULL); 93 | test_all!(c.seq_0("_concat(cast(v as blob))"), NULL); 94 | 95 | test_all!(c.seq_1("_concat(cast(v as text))"), blob("1")); 96 | test_all!(c.seq_1("_concat(cast(v as blob))"), blob("1")); 97 | test_all!( 98 | c.seq_1("_concat(cast(v as text), cast((v+1) as blob))"), 99 | blob("12") 100 | ); 101 | 102 | let expected = (1..=1000).map(|i| i.to_string()).collect::(); 103 | test_all!(c.seq_1000("_concat(cast(v as text))"), blob(expected)); 104 | test_all!(c.seq_1000("_concat(cast(v as blob))"), blob(expected)); 105 | 106 | let expected = (1..=1000).fold(String::new(), |mut output, i| { 107 | let _ = write!(output, "{i}{}", i + 1); 108 | output 109 | }); 110 | test_all!( 111 | c.seq_1000("_concat(null, cast(v as text), cast((v+1) as blob), null)"), 112 | blob(expected) 113 | ); 114 | } 115 | 116 | #[test] 117 | #[cfg(feature = "hex")] 118 | fn concat_sequence_hex() { 119 | let c = Conn::new(); 120 | 121 | test_all!(c.seq_0("_concat_hex(cast(v as text))"), NULL); 122 | test_all!(c.seq_0("_concat_hex(cast(v as blob))"), NULL); 123 | 124 | test_all!(c.seq_1("_concat_hex(cast(NULL as text))"), EMPTY); 125 | test_all!(c.seq_1("_concat_hex(cast(NULL as blob))"), EMPTY); 126 | 127 | test_all!(c.seq_1("_concat_hex(cast(v as text))"), hex("1")); 128 | test_all!(c.seq_1("_concat_hex(cast(v as blob))"), hex("1")); 129 | test_all!( 130 | c.seq_1("_concat_hex(cast(v as text), cast((v+1) as blob))"), 131 | hex("12") 132 | ); 133 | 134 | let expected = (1..=1000).map(|i| i.to_string()).collect::(); 135 | test_all!(c.seq_1000("_concat_hex(cast(v as text))"), hex(expected)); 136 | test_all!(c.seq_1000("_concat_hex(cast(v as blob))"), hex(expected)); 137 | 138 | let expected = (1..=1000).fold(String::new(), |mut output, i| { 139 | let _ = write!(output, "{i}{}", i + 1); 140 | output 141 | }); 142 | test_all!( 143 | c.seq_1000("_concat_hex(null, cast(v as text), cast((v+1) as blob), null)"), 144 | hex(expected) 145 | ); 146 | } 147 | -------------------------------------------------------------------------------- /tests/scalar.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | #[macro_use] 4 | #[path = "_utils.rs"] 5 | mod utils; 6 | use crate::utils::Conn; 7 | 8 | #[ctor::ctor] 9 | fn init() { 10 | let _ = env_logger::builder().is_test(true).try_init(); 11 | } 12 | 13 | #[test] 14 | fn simple() { 15 | let c = Conn::new(); 16 | test_all!(c.select("(NULL)"), NULL); 17 | test_all!(c.select("(NULL, NULL, NULL)"), NULL); 18 | test_all!(c.select("(1)"), ERROR); 19 | test_all!(c.select("(0.42)"), ERROR); 20 | test_all!(c.select("()"), ERROR); 21 | test_all!(c.select("('')"), blob("")); 22 | test_all!(c.select("('a')"), blob("a")); 23 | test_all!(c.select("('123456789')"), blob("123456789")); 24 | test_all!(c.select("(x'')"), blob("")); 25 | test_all!(c.select("(x'00')"), blob("\0")); 26 | test_all!( 27 | c.select("(x'0123456789abcdef')"), 28 | bytes_as_blob(b"\x01\x23\x45\x67\x89\xab\xcd\xef") 29 | ); 30 | test_all!( 31 | c.select("('', 'a', '123456789', x'', x'00', x'0123456789abcdef')"), 32 | bytes_as_blob(b"a123456789\x00\x01\x23\x45\x67\x89\xab\xcd\xef") 33 | ); 34 | test_all!( 35 | c.select("(NULL, 'a', NULL, '123456789', x'', x'00', NULL, x'0123456789abcdef', NULL)"), 36 | bytes_as_blob(b"a123456789\x00\x01\x23\x45\x67\x89\xab\xcd\xef") 37 | ); 38 | } 39 | 40 | #[test] 41 | #[cfg(feature = "hex")] 42 | fn simple_hex() { 43 | let c = Conn::new(); 44 | test_all!(c.select("_hex(NULL)"), EMPTY); 45 | test_all!(c.select("_hex(NULL, NULL, NULL)"), EMPTY); 46 | test_all!(c.select("_hex(1)"), ERROR); 47 | test_all!(c.select("_hex(0.42)"), ERROR); 48 | test_all!(c.select("_hex()"), ERROR); 49 | test_all!(c.select("_hex('')"), hex("")); 50 | test_all!(c.select("_hex('a')"), hex("a")); 51 | test_all!(c.select("_hex('123456789')"), hex("123456789")); 52 | test_all!(c.select("_hex(x'')"), hex("")); 53 | test_all!(c.select("_hex(x'00')"), hex("\0")); 54 | test_all!( 55 | c.select("_hex(x'0123456789abcdef')"), 56 | bytes_as_hex(b"\x01\x23\x45\x67\x89\xab\xcd\xef") 57 | ); 58 | test_all!( 59 | c.select("_hex('', 'a', '123456789', x'', x'00', x'0123456789abcdef')"), 60 | bytes_as_hex(b"a123456789\x00\x01\x23\x45\x67\x89\xab\xcd\xef") 61 | ); 62 | test_all!( 63 | c.select("_hex(NULL, 'a', NULL, '123456789', x'', x'00', NULL, x'0123456789abcdef', NULL)"), 64 | bytes_as_hex(b"a123456789\x00\x01\x23\x45\x67\x89\xab\xcd\xef") 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /tests/test-ext.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | SQLITE3_BIN=${SQLITE3_BIN:-sqlite3} 5 | EXTENSION_FILE=${EXTENSION_FILE:-target/debug/examples/libsqlite_hashes} 6 | 7 | if [ ! -f "$EXTENSION_FILE" ] && [ ! -f "$EXTENSION_FILE.so" ] && [ ! -f "$EXTENSION_FILE.dylib" ] && [ ! -f "$EXTENSION_FILE.dll" ]; then 8 | echo "Extension file $EXTENSION_FILE [.so|.dylib|.dll] do not exist. Run 'just build-ext' first. Available files:" 9 | ls -l "$EXTENSION_FILE"* 10 | exit 1 11 | fi 12 | echo "Using extension file '$EXTENSION_FILE [.so|.dylib|.dll]'" 13 | 14 | if ! command -v "$SQLITE3_BIN" > /dev/null; then 15 | echo "$SQLITE3_BIN executable could not be found" 16 | exit 1 17 | fi 18 | echo "Found $SQLITE3_BIN executable $($SQLITE3_BIN --version)" 19 | 20 | test_one() { 21 | local sql=$1 22 | local expected=$2 23 | 24 | echo "Trying to get '$expected' from $sql" 25 | result=$($SQLITE3_BIN <