From 2c9a26956e95e39fb97c47f77f9db96857c1df47 Mon Sep 17 00:00:00 2001 From: Lucid Programmer Date: Tue, 10 Nov 2026 15:01:14 +0530 Subject: [PATCH 1/2] storage: add redb 4.1.0 upstream source with tests and examples --- v2/redb/.cargo/config.toml | 8 + v2/redb/.cargo_vcs_info.json | 6 + v2/redb/.dockerignore | 11 + v2/redb/.editorconfig | 10 + v2/redb/.github/workflows/ci.yml | 131 + v2/redb/.gitignore | 28 + v2/redb/AGENTS.md | 64 + v2/redb/CHANGELOG.md | 556 ++++ v2/redb/CLAUDE.md | 1 + v2/redb/Cargo.lock | 832 +++++ v2/redb/Cargo.toml | 118 + v2/redb/Cargo.toml.orig | 56 + v2/redb/Dockerfile.bench | 15 + v2/redb/LICENSE-APACHE | 201 ++ v2/redb/LICENSE-MIT | 19 + v2/redb/README.md | 92 + v2/redb/build.rs | 10 + v2/redb/clippy.toml | 3 + v2/redb/deny.toml | 144 + v2/redb/docs/design.md | 493 +++ v2/redb/examples/bincode_keys.rs | 112 + v2/redb/examples/derive_value_impl.rs | 31 + v2/redb/examples/int_keys.rs | 20 + v2/redb/examples/multithread.rs | 84 + v2/redb/examples/special_values.rs | 123 + v2/redb/justfile | 84 + v2/redb/rust-toolchain | 1 + v2/redb/rustfmt.toml | 1 + v2/redb/src/backends.rs | 2 + v2/redb/src/complex_types.rs | 93 + v2/redb/src/db.rs | 1696 ++++++++++ v2/redb/src/error.rs | 693 ++++ v2/redb/src/lib.rs | 108 + v2/redb/src/multimap_table.rs | 1653 ++++++++++ v2/redb/src/sealed.rs | 1 + v2/redb/src/table.rs | 670 ++++ v2/redb/src/transaction_tracker.rs | 316 ++ v2/redb/src/transactions.rs | 2287 +++++++++++++ v2/redb/src/tree_store/btree.rs | 1120 +++++++ v2/redb/src/tree_store/btree_base.rs | 1725 ++++++++++ v2/redb/src/tree_store/btree_iters.rs | 811 +++++ v2/redb/src/tree_store/btree_mutator.rs | 1076 ++++++ v2/redb/src/tree_store/mod.rs | 25 + v2/redb/src/tree_store/page_store/backends.rs | 107 + v2/redb/src/tree_store/page_store/base.rs | 376 +++ v2/redb/src/tree_store/page_store/bitmap.rs | 534 +++ .../tree_store/page_store/buddy_allocator.rs | 540 +++ .../src/tree_store/page_store/cached_file.rs | 656 ++++ .../src/tree_store/page_store/fast_hash.rs | 34 + .../page_store/file_backend/fallback.rs | 51 + .../tree_store/page_store/file_backend/mod.rs | 9 + .../page_store/file_backend/optimized.rs | 191 ++ v2/redb/src/tree_store/page_store/header.rs | 712 ++++ v2/redb/src/tree_store/page_store/layout.rs | 250 ++ .../src/tree_store/page_store/lru_cache.rs | 103 + v2/redb/src/tree_store/page_store/mod.rs | 30 + .../src/tree_store/page_store/page_manager.rs | 1231 +++++++ v2/redb/src/tree_store/page_store/region.rs | 203 ++ .../src/tree_store/page_store/savepoint.rs | 195 ++ v2/redb/src/tree_store/page_store/xxh3.rs | 825 +++++ v2/redb/src/tree_store/table_tree.rs | 785 +++++ v2/redb/src/tree_store/table_tree_base.rs | 507 +++ v2/redb/src/tuple_types.rs | 448 +++ v2/redb/src/types.rs | 665 ++++ v2/redb/src/types/chrono_v0_4.rs | 547 ++++ v2/redb/src/types/uuid.rs | 83 + v2/redb/tests/backward_compatibility.rs | 339 ++ v2/redb/tests/basic_tests.rs | 1962 +++++++++++ v2/redb/tests/integration_tests.rs | 2916 +++++++++++++++++ v2/redb/tests/multimap_tests.rs | 419 +++ v2/redb/tests/multithreading_tests.rs | 115 + 71 files changed, 30363 insertions(+) create mode 100644 v2/redb/.cargo/config.toml create mode 100644 v2/redb/.cargo_vcs_info.json create mode 100644 v2/redb/.dockerignore create mode 100644 v2/redb/.editorconfig create mode 100644 v2/redb/.github/workflows/ci.yml create mode 100644 v2/redb/.gitignore create mode 100644 v2/redb/AGENTS.md create mode 100644 v2/redb/CHANGELOG.md create mode 100644 v2/redb/CLAUDE.md create mode 100644 v2/redb/Cargo.lock create mode 100644 v2/redb/Cargo.toml create mode 100644 v2/redb/Cargo.toml.orig create mode 100644 v2/redb/Dockerfile.bench create mode 100644 v2/redb/LICENSE-APACHE create mode 100644 v2/redb/LICENSE-MIT create mode 100644 v2/redb/README.md create mode 100644 v2/redb/build.rs create mode 100644 v2/redb/clippy.toml create mode 100644 v2/redb/deny.toml create mode 100644 v2/redb/docs/design.md create mode 100644 v2/redb/examples/bincode_keys.rs create mode 100644 v2/redb/examples/derive_value_impl.rs create mode 100644 v2/redb/examples/int_keys.rs create mode 100644 v2/redb/examples/multithread.rs create mode 100644 v2/redb/examples/special_values.rs create mode 100644 v2/redb/justfile create mode 100644 v2/redb/rust-toolchain create mode 100644 v2/redb/rustfmt.toml create mode 100644 v2/redb/src/backends.rs create mode 100644 v2/redb/src/complex_types.rs create mode 100644 v2/redb/src/db.rs create mode 100644 v2/redb/src/error.rs create mode 100644 v2/redb/src/lib.rs create mode 100644 v2/redb/src/multimap_table.rs create mode 100644 v2/redb/src/sealed.rs create mode 100644 v2/redb/src/table.rs create mode 100644 v2/redb/src/transaction_tracker.rs create mode 100644 v2/redb/src/transactions.rs create mode 100644 v2/redb/src/tree_store/btree.rs create mode 100644 v2/redb/src/tree_store/btree_base.rs create mode 100644 v2/redb/src/tree_store/btree_iters.rs create mode 100644 v2/redb/src/tree_store/btree_mutator.rs create mode 100644 v2/redb/src/tree_store/mod.rs create mode 100644 v2/redb/src/tree_store/page_store/backends.rs create mode 100644 v2/redb/src/tree_store/page_store/base.rs create mode 100644 v2/redb/src/tree_store/page_store/bitmap.rs create mode 100644 v2/redb/src/tree_store/page_store/buddy_allocator.rs create mode 100644 v2/redb/src/tree_store/page_store/cached_file.rs create mode 100644 v2/redb/src/tree_store/page_store/fast_hash.rs create mode 100644 v2/redb/src/tree_store/page_store/file_backend/fallback.rs create mode 100644 v2/redb/src/tree_store/page_store/file_backend/mod.rs create mode 100644 v2/redb/src/tree_store/page_store/file_backend/optimized.rs create mode 100644 v2/redb/src/tree_store/page_store/header.rs create mode 100644 v2/redb/src/tree_store/page_store/layout.rs create mode 100644 v2/redb/src/tree_store/page_store/lru_cache.rs create mode 100644 v2/redb/src/tree_store/page_store/mod.rs create mode 100644 v2/redb/src/tree_store/page_store/page_manager.rs create mode 100644 v2/redb/src/tree_store/page_store/region.rs create mode 100644 v2/redb/src/tree_store/page_store/savepoint.rs create mode 100644 v2/redb/src/tree_store/page_store/xxh3.rs create mode 100644 v2/redb/src/tree_store/table_tree.rs create mode 100644 v2/redb/src/tree_store/table_tree_base.rs create mode 100644 v2/redb/src/tuple_types.rs create mode 100644 v2/redb/src/types.rs create mode 100644 v2/redb/src/types/chrono_v0_4.rs create mode 100644 v2/redb/src/types/uuid.rs create mode 100644 v2/redb/tests/backward_compatibility.rs create mode 100644 v2/redb/tests/basic_tests.rs create mode 100644 v2/redb/tests/integration_tests.rs create mode 100644 v2/redb/tests/multimap_tests.rs create mode 100644 v2/redb/tests/multithreading_tests.rs diff --git a/v2/redb/.cargo/config.toml b/v2/redb/.cargo/config.toml new file mode 100644 index 000000000..bba398c38 --- /dev/null +++ b/v2/redb/.cargo/config.toml @@ -0,0 +1,8 @@ +[target.wasm32-wasip1] +runner = "wasmtime --dir=/tmp" + +[target.wasm32-wasip1-threads] +runner = "wasmtime --dir=/tmp -W threads=y -S threads=y" + +[target.wasm32-wasip2] +runner = "wasmtime --dir=/tmp" diff --git a/v2/redb/.cargo_vcs_info.json b/v2/redb/.cargo_vcs_info.json new file mode 100644 index 000000000..7c6e254a9 --- /dev/null +++ b/v2/redb/.cargo_vcs_info.json @@ -0,0 +1,6 @@ +{ + "git": { + "sha1": "6ed1f981ba4deab0b2adbdd7bccb46ec409b2191" + }, + "path_in_vcs": "" +} \ No newline at end of file diff --git a/v2/redb/.dockerignore b/v2/redb/.dockerignore new file mode 100644 index 000000000..a7a9f0ea9 --- /dev/null +++ b/v2/redb/.dockerignore @@ -0,0 +1,11 @@ +* +!src/ +!crates/ +!tests/ +!examples/ +!cargo/ +!./*.toml +!./build.rs +!./rust-toolchain +!./LICENSE +**/target/ diff --git a/v2/redb/.editorconfig b/v2/redb/.editorconfig new file mode 100644 index 000000000..5e8de0084 --- /dev/null +++ b/v2/redb/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 100 +trim_trailing_whitespace = true diff --git a/v2/redb/.github/workflows/ci.yml b/v2/redb/.github/workflows/ci.yml new file mode 100644 index 000000000..50f0fe72a --- /dev/null +++ b/v2/redb/.github/workflows/ci.yml @@ -0,0 +1,131 @@ +name: CI + +on: + pull_request: + push: # required for actions/cache to work + branches: + - master + +jobs: + ci: + strategy: + matrix: + os: ["ubuntu-latest", "macos-latest", "windows-latest", "ubuntu-24.04-arm"] + + runs-on: ${{ matrix.os }} + + env: + RUSTFLAGS: --deny warnings + + steps: + - uses: actions/checkout@v4 + - name: Cache + id: rust-cache + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + fuzz/target/ + key: ${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/Cargo.toml', '.github/workflows/*.yml', 'rust-toolchain') }} + + - name: Non ASCII characters + if: startsWith(matrix.os, 'ubuntu') + run: | + if grep -P -n "[^\x00-\x7F]" -r --include="*.rs" --include="*.toml" .; then + echo "Non-ASCII characters detected" + exit 1 + fi + + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Rust + run: | + rustup component add rustfmt + rustup component add clippy + + - name: OSX x86 rust + if: startsWith(matrix.os, 'macos') + run: | + # For some reason this is required to run the fuzzer on OSX + rustup target add x86_64-apple-darwin + + - name: Setup wasmtime + uses: bytecodealliance/actions/wasmtime/setup@v1 + with: + version: "24.0.0" + + - name: Install cargo-deny + if: steps.rust-cache.outputs.cache-hit != 'true' + run: cargo install --force --version 0.16.2 cargo-deny --locked + + - name: Install cargo-fuzz + if: steps.rust-cache.outputs.cache-hit != 'true' + run: cargo install --force --version 0.12.0 cargo-fuzz --locked + + - name: Install just + if: steps.rust-cache.outputs.cache-hit != 'true' + run: cargo install --force --version 1.36.0 just --locked + + - name: Format + run: cargo fmt --all -- --check + + - name: Compile (default features) + run: cargo check --all --all-targets + + - name: Compile (all features) + run: cargo check --all --all-targets --all-features + + - name: Compile (no features) + run: cargo check --all --all-targets --no-default-features + + - name: Compile (`wasm32-unknown-unknown`) + if: matrix.os == 'ubuntu-latest' + run: | + rustup target add wasm32-unknown-unknown + # Not possible to set default members for specific a target: + # https://github.com/rust-lang/cargo/issues/6179 + command="cargo check -p $(cargo pkgid) -p redb-derive --target wasm32-unknown-unknown" + $command + $command --all-features + $command --no-default-features + rustup target remove wasm32-unknown-unknown + + - name: Clippy + run: cargo clippy --all --all-targets -- -Dwarnings + + - name: Fuzzer + if: startsWith(matrix.os, 'ubuntu') || startsWith(matrix.os, 'macos') + run: just fuzz_ci + + - name: Run tests + run: just test_all + + - name: Run CPython wrapper tests + if: runner.os != 'Windows' + run: | + python3 -m venv venv + source venv/bin/activate + pip3 install --upgrade pip + pip3 install maturin + just test_py + + - name: Run CPython wrapper tests + if: runner.os == 'Windows' + run: | + python3 -m venv venv + venv\Scripts\activate + pip3 install --upgrade pip + pip3 install maturin + just test_py + + - name: Run WASI tests + if: runner.os != 'Windows' + run: | + RUSTFLAGS="" just test_wasi diff --git a/v2/redb/.gitignore b/v2/redb/.gitignore new file mode 100644 index 000000000..2f45981fe --- /dev/null +++ b/v2/redb/.gitignore @@ -0,0 +1,28 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# IntelliJ +.idea/ +**/*.iml + + +**/__pycache__ +**/*.pyc + +# Profiling +perf.data* +flamegraph.svg + +# benchmark and test temporary files +/.tmp* +.vscode/settings.json + +**/*.redb diff --git a/v2/redb/AGENTS.md b/v2/redb/AGENTS.md new file mode 100644 index 000000000..a5ff9b37a --- /dev/null +++ b/v2/redb/AGENTS.md @@ -0,0 +1,64 @@ +# Agent instructions for redb + +This file tells coding agents how to work productively in this repository. + +## Setup + +The Rust toolchain is pinned by `rust-toolchain`, so `rustup` +will pick the right version automatically. Beyond that, install the following +tools before doing anything else: + +- `just` (task runner): `cargo install --locked just` +- `cargo-deny` (license / advisory checks): `cargo install --locked cargo-deny` +- `cargo-fuzz` (fuzzing harness driver): `cargo install --locked cargo-fuzz` +- `rustfmt` and `clippy` components: `rustup component add rustfmt clippy` +- System packages: `libclang-dev` (required by transitive build deps on Linux) + +The CI workflow pins specific versions; prefer those versions if you hit incompatibilities. See +`.github/workflows/ci.yml` for the exact list CI uses. + +Python tests (`just test_py`) additionally need `maturin` and Python; only +install these if you are touching `crates/redb-python`. + +## Before completing a task + +**Always run `just test` and confirm it passes before telling the user you are done.** +This target runs the `pre` recipe first, which executes `cargo deny check licenses`, +`cargo fmt --check`, and `cargo clippy --all-targets --all-features`, and then +runs `cargo test --all-features` with `RUST_BACKTRACE=1`. If any of those fail, +fix the underlying issue — do not bypass checks. + +If you are touching workspace crates beyond the main `redb` crate, run +`just test_all` instead, which also builds and tests the full workspace. + +## Fuzzing + +`just fuzz` runs the libFuzzer-based harness under `fuzz/` (`fuzz_redb`) with +`-max_len=10000` and no sanitizer. Use it (with an appropriate time limit) when +changing on-disk format, B-tree logic, transaction/savepoint code, or anything else +where corner cases matter. Related targets: `just fuzz_ci` (60s +smoke run), `just fuzz_coverage` (HTML coverage report). + +## Benchmarks + +`just bench` runs `redb-bench`'s default benchmark (`redb_benchmark`). Pass a +different bench name to compare against other engines, e.g. +`just bench lmdb_benchmark`. `just bench_containerized` runs benchmarks inside +a memory-limited Docker container (see `Dockerfile.bench`). + +## Design / architecture + +The design doc lives at `docs/design.md` — read it before making non-trivial +changes to the storage format, transaction layer, or B-tree code. The main +crate source is in `src/`, additional workspace crates (bench harness, Python +bindings, derive macro) are under `crates/`, integration tests in `tests/`, +and fuzz targets in `fuzz/`. + +## Other notes + +- The repo enforces ASCII-only source: CI fails on non-ASCII characters in + `*.rs` and `*.toml` files. Keep new code ASCII-only. +- `RUSTFLAGS=--deny warnings` is set in CI, so any new warning will break the + build. Fix warnings rather than silencing them. +- The file format is stable; changes that affect on-disk layout require an + upgrade path and usually a `CHANGELOG.md` entry. diff --git a/v2/redb/CHANGELOG.md b/v2/redb/CHANGELOG.md new file mode 100644 index 000000000..32fe717f8 --- /dev/null +++ b/v2/redb/CHANGELOG.md @@ -0,0 +1,556 @@ +# redb - Changelog + +## 4.1.0 - 2026-04-19 +**This release contains a large number of bug fixes discovered by AI coding agents** + +* Fix a bug where `MultimapValue::len()` and `is_empty()` returned stale counts after + consuming entries via `next_back()`. +* Fix a bug where `restore_savepoint()` used in a non-`Immediate` durability transaction and when + there are persistent savepoints newer than the one being restored, could fail + with `SavepointError::InvalidSavepoint`, but the savepoint would actually be partially applied. + The call now fails up front with `SavepointError::ImmediateDurabilityRequired`. +* Fix a bug in `restore_savepoint()` where modifications made earlier in the transaction might + not be reverted. +* Fix a bug where renaming a table that was already modified in the same transaction could cause + the database to become corrupted. +* Fix a bug where calling `restore_savepoint()` after modifying a table in the same + transaction could cause the table to become corrupted in a future transaction. +* Fix a panic when `delete_table()` was called on a table that had been modified in the same + transaction. +* Fix a panic in `restore_savepoint()` when passed a `Savepoint` from a different `Database`. + `SavepointError::InvalidSavepoint` is now returned instead. +* Fix a bug where a transaction that created a persistent savepoint and was then + aborted could cause the database file to grow excessively, until the `Database` was dropped. +* Fix a panic in `check_integrity()` when called while another transaction is still alive. + `DatabaseError::TransactionInProgress` is now returned instead. +* Fix a bug where aborting a transaction that called `restore_savepoint()` with a savepoint + when a newer savepoint existed could cause database space to be leaked. +* Fix a bug where aborting a transaction that called `restore_savepoint()` would leave more + recent savepoints invalid. +* Improve performance when reading concurrently from multiple threads. Around 15% speedup on some benchmarks. +* Optimize cache usage, and general write performance. Around 1.5x speedup on some benchmarks. +* Optimize memory usage. +* Other performance optimizations. + +## 4.0.0 - 2026-04-02 +* Implement `Drop` on `AccessGuardMut` and `AccessGuardMutInPlace`, which requires that these be dropped + before the `Table` they borrow from. + This fixes a critical bug where the accessor could outlive the `Table`, and be dropped after the + transaction had already committed. This could cause data loss due to the data in the accessor + being written out after the transaction had already completed. +* Remove `Legacy` type. To migrate off the `Legacy` type, use the `Legacy` type in the 3.x release + and copy the data to a table with plain tuples, before upgrading to the 4.x release. + +## 3.1.3 - 2026-04-02 +* Fix a data loss bug which can occur when the guard returned from `Table::get_mut()` is dropped + after the transaction has been committed. +* Add a warning to `Table::insert_reserve()` indicating that it can cause data loss and recommending + to upgrade to the 4.0.0 release. + +## 3.1.2 - 2026-04-01 +* Reduce memory usage of open databases + +## 3.1.1 - 2026-03-08 +* Fix panic which could occur when inserting into a table with fixed size keys when `debug_assertions` are enabled +* Add additional information to the stats returned by `cache_stats()` + +## 3.1.0 - 2025-09-25 +* Implement `std::error::Error` for `SetDurabilityError` +* Fix compilation error on various non-tier-1 platforms, such as wasm32-unknown + +## 3.0.2 - 2025-09-16 +* Fix performance issue where a transaction with a large number of writes would cause + `WriteTransaction::abort()` and committing non-durable transactions to become slow. + +## 3.0.1 - 2025-08-23 +* Fix correctness issue with `range()`, `extract_from_if()`, and `retain_in()`. If a RangeBounds + with `start` > `end` was passed as an argument and `start` and `end` keys were stored in different + internal pages in the database (i.e. a sufficient condition is that more than 4KiB of key-value + pairs were between the two keys) then these methods would perform as if the argument had been + `start..` +* Fix performance regression, from redb 2.x, where `Durability::None` commits could become linearly + slower during a series of transactions. + +## 2.6.3 - 2025-08-23 +* Fix correctness issue with `range()`, `extract_from_if()`, and `retain_in()`. If a RangeBounds + with `start` > `end` was passed as an argument and `start` and `end` keys were stored in different + internal pages in the database (i.e. a sufficient condition is that more than 4KiB of key-value + pairs were between the two keys) then these methods would perform as if the argument had been + `start..` + +## 1.5.2 - 2025-08-23 +* Fix correctness issue with `range()`, `drain()`, and `drain_filter()`. If a RangeBounds + with `start` > `end` was passed as an argument and `start` and `end` keys were stored in different + internal pages in the database (i.e. a sufficient condition is that more than 4KiB of key-value + pairs were between the two keys) then these methods would perform as if the argument had been + `start..` + +## 3.0.0 - 2025-08-09 + +### Removes support for file format v2. +Use `Database::upgrade()`, in redb 2.6, to migrate to the v3 file format. + +### General storage optimizations +The v3 file format has been further optimized to reduce the size of the database. Databases with only +a few small keys will see the largest benefit, and the minimum size of a database file has decreased +from ~2.5MiB to ~50KiB. To achieve the smallest file size call `Database::compact()` before +dropping the `Database`. + +Additionally, performance is ~15% better in bulk load benchmarks. This was achieved by implementing +a custom hash function for various in-memory `HashSet`s and `HashMap`s, and by optimizing the usage +of buffers held in `Arc`s to reduce the number of atomic instructions executed. + +### Optimize storage of tuple types +Storage of variable width tuple types with arity greater than 1 is more efficient. The new format +elides the length of any fixed width fields and uses varint encoding for the lengths of all variable +width fields. + +Note that this encoding is not compatible with the serialization of variable width tuples used in prior versions. +To load tuple data created prior to version 3.0, wrap them in the `Legacy` type. +For example, `TableDefinition` becomes `TableDefinition>`. +Fixed width tuples, such as `(u32, u64)` are backwards compatible. + +### Derive for Key and Value traits +`Key` and `Value` can be derived using the `redb-derive` crate. Note that it does not support +schema migration. The recommended pattern to migrate schema is to create a new table, and then +perform a migration from the old table to the new table. + +### Read-only multi-process support +Multiple processes may open the same database file for reading by using the new `ReadOnlyDatabase` +type. On platforms which support file locks, this acquires a shared lock on the database file. + +### Enable garbage collection in Durability::None transactions +Non-durable transactions will now free pages when possible (pages allocated in a preceding +non-durable transaction which are no longer referenced). +This resolves an issue where a long sequence of non-durable transactions led to significant growth +in the size of the database file. +This change increases the RAM required for a sequence of non-durable transactions, such that RAM +proportional to the net change in the database is now used. However, it will never use more than +about 0.2% of the database file size. + +### Other changes + +* Add `ReadOnlyDatabase` +* Add `Builder::open_read_only()` +* Add `StorageBackend::close()` +* Add `Table::get_mut()` +* Add `chrono_v0_4` feature flag which enables serialization of the `NaiveDate`, `NaiveTime`, + `NaiveDatetime`, `DateTime`, and `FixedOffset` types in the `chrono` crate +* Add `uuid` feature flag which enables serialization of the `Uuid` type in the `uuid` crate +* Change `StorageBackend::read()` to accept a `&mut [u8]` output argument instead of returning + a `Vec` +* Change `Table::insert_reserve()` to take `usize` instead of `u32` as the argument type +* Change `TypeName::name()` to be public +* Change `ReadTransactionStillInUse` to contain a `Box` +* Change `set_durability()` to return a `Result` +* Move `Database::cache_stats()` and `Database::begin_read()` to `ReadableDatabase` trait +* Rename `AccessGuardMut` to `AccessGuardMutInPlace`. Note that a new `AccessGuardMut` struct has + been added; it serves a different purpose +* Remove `Durability::Paranoid` +* Fix a rare case where `check_integrity()` returned `Ok(false)` even though no repair was required, + when called on a database that was not shutdown cleanly and was automatically repaired when opened +* Disallow access to the database from read transactions after the `Database` as been + dropped. Access will now return `DatabaseClosed` + +## 2.6.2 - 2025-08-02 +* Forward compatibility improvement which makes the file format more flexible to support a potential + future optimization + +## 2.6.1 - 2025-07-24 +* Fix a forward compatibility issue which caused a crash when opening databases created with redb + 3.x. Note that opening 3.x databases with redb 2.x is not generally supported and only works + in certain situations. + +## 2.6.0 - 2025-05-22 + +### Add support for the v3 file format. +This file format improves savepoints. +Savepoints in the v3 format have constant, and small, overhead. Creating +and restoring them is also much faster. The v3 file format also supports +savepoints on large databases (v2 has a limit around 32TB). This release +creates v2 databases by default. Use `Builder::create_with_file_format_v3()` +and `Database::upgrade()`, respectively, to enable and migrate to v3. + +**The upcoming 3.0 release will only support the v3 file format.** + +* Add `Builder::create_with_file_format_v3()` +* Add `Database::upgrade()` + +## 2.5.0 - 2025-04-21 +* Add `rename_table()` and `rename_multimap_table()` +* Add `Key` and `Value` implementations for the unary tuple type (i.e. `(T,)`) +* Fix an issue which could cause a panic when concurrently performing read and write transactions, + when `debug_assertions` were enabled +* Optimize `retain()` and `retain_in()` to use less space in the database file +* Improve handling of some internal errors to return `LockPoisoned` instead of panicking + +## 2.4.0 - 2024-12-30 +* Add `Database::cache_stats()` +* Fix `open()` and `create()` to return `InvalidData` when they are called on a database file + that is not a valid redb database +* Significantly speed up `restore_savepoint()`. The time is takes now scales with the change delta + since the savepoint was captured, rather than the size of the database file +* `DatabaseStats::fragmented_bytes()` is now more accurate + +## 2.3.0 - 2024-12-10 +* Add `WriteTransaction::set_two_phase_commit()` +* Add `WriteTransaction::set_quick_repair()` which enables a faster repair mechanism at the cost of + slower transaction commits +* `Durability::Paranoid` is now deprecated. Use `set_two_phase_commit(true)` instead +* Fix various bugs when repairing the database after an unclean shutdown. These could result in + panics, leaked space in the database file, or database corruption + +## 2.2.0 - 2024-10-27 +* Implement `TableHandle` for `ReadOnlyTable` +* Fix bug in write cache, which caused pages to be evicted randomly. Pages are now evicted based on + how recently they have been accessed + +## 2.1.4 - 2024-10-10 +* Optimize `first()` and `last()` to be almost 2x faster +* Improve in-memory cache algorithm to resolve edge cases where certain pages could become + uncacheable under cache pressure +* Fix bug in read cache where the read cache could become disabled. This was likely to occur in + multithreaded workloads when the read cache was smaller than the database file. This bug lead to + 5-10x performance degradations for some workloads + +## 2.1.3 - 2024-09-14 +* Significant performance optimizations to `compact()` +* Fix some additional cases where `compact()` did not fully compact the database +* Fix a panic that could occur in `commit()` or `abort()` after an IO error. `StorageError::PreviousIo` is now returned +* Fix a potential panic that could occur when repairing the database after a crash + +## 2.1.2 - 2024-08-25 +### Major fixes: +* Fix leak of database space that could occur when calling `restore_savepoint()` +* Fix leak of database space when calling `delete_multimap_table()` +* Fix database corruption which could occur when restoring a savepoint. This edge case is rare, + and could only occur if the database was less than approximately 4TiB when the savepoint was + created, and greater than 4TiB when the savepoint was restored +* Fix edge case where a transient I/O error that occurred during transaction commit, but then did + not reoccur when the `Database` was dropped, could cause database corruption + +**Important: If your application has called `restore_savepoint()`, `delete_multimap_table()`, +or you suspect it may have experienced a transient I/O error during transaction commit. +It is recommended that you run `check_integrity()` after upgrading to this version. +This will both detect corruption and clean up any leaked space.** + +### Other changes and fixes: +* Optimize page freeing to reduce the size of the database file +* Fix several cases where `check_integrity()` would return `Ok(false)` instead of `Ok(true)` +* Fix some cases where `compact()` did not fully compact the database +* Make the metadata overhead returned by `WriteTransaction::stats()` more accurate +* Return `StorageError::ValueTooLarge` when a key-value pair exceeds a total of 3.75GiB. + Previously, a panic would occur for key-value pairs that were approximately 4GiB. +* Downgrade several `info!` log messages to `debug!` +* Improve documentation + +## 2.1.1 - 2024-06-09 +* Fix panic that occurred when calling `compact()` when a read transaction was in progress +* Fix `ReadTransaction::close()` to return `Ok` when it succeeds +* Performance optimizations + +## 2.1.0 - 2024-04-20 +* Implement `Key` and `Value` for `String` +* Allow users to implement `ReadableTableMetadata`, `ReadableTable`, and `ReadableMultimapTable` + +## 2.0.0 - 2024-03-22 + +### Major file format change +2.0.0 uses a new file format that optimizes `len()` to be constant time. This means that it is not +backwards compatible with 1.x. To upgrade, consider using a pattern like that shown in the +[upgrade_v1_to_v2](https://github.com/cberner/redb/blob/222a37f4600588261b0983eebcd074bb69d6e5a0/tests/backward_compatibility.rs#L282-L299) test. + +### Other changes +* `check_integrity()` now returns a `DatabaseError` instead of a `StorageError` +* Table metadata methods have moved to a new `ReadableTableMetadata` trait +* Rename `RedbKey` to `Key` +* Rename `RedbValue` to `Value` +* Remove lifetimes from read-only tables +* Remove lifetime from `WriteTransaction` and `ReadTransaction` +* Remove `drain()` and `drain_filter()` from `Table`. Use `retain`, `retain_in`, `extract_if` or `extract_from_if` instead +* impl `Clone` for `Range` +* Add support for `[T;N]` as a `Value` or `Key` as appropriate for the type `T` +* Add `len()` and `is_empty()` to `MultimapValue` +* Add `retain()` and `retain_in()` to `Table` +* Add `extract_if()` and `extract_from_if()` to `Table` +* Add `range()` returning a `Range` with the `'static` lifetime to read-only tables +* Add `get()` returning a range with the `'static` lifetime to read-only tables +* Add `close()` method to `ReadTransaction` + +## 1.5.1 - 2024-03-16 +* Fix `check_integrity()` so that it returns `Ok(true)` when no repairs were preformed. Previously, + it returned `Ok(false)` + +## 1.5.0 - 2024-01-15 +* Export `TableStats` type +* Export `MutInPlaceValue` which allows custom types to support `insert_reserve()` +* Add untyped table API which allows metadata, such as table stats, to be retrieved for at table + without knowing its type at compile time +* Fix compilation on uncommon platforms (those other than Unix and Windows) + +## 1.4.0 - 2023-11-21 +* Add `Builder::set_repair_callback()` which can be used to set a callback function that will be invoked if the database needs repair while opening it. +* Add support for custom storage backends. This is done by implementing the `StorageBackend` trait and + using the `Builder::create_with_backend` function. This allows the database to be stored in a location other + than the filesystem +* Implement `RedbKey` and `RedbValue` for `char` +* Implement `RedbKey` and `RedbValue` for `bool` +* Implement `TableHandle` for `Table` +* Implement `MultimapTableHandle` for `MultimapTable` +* Fix panic that could occur when inserting a large number of fixed width values into a table within a single transaction +* Fix panic when calling `delete_table()` on a table that is already open +* Improve performance for fixed width types +* Support additional platforms + +## 1.3.0 - 2023-10-22 +* Implement `RedbKey` for `Option` +* Implement `RedbValue` for `Vec` +* Implement `Debug` for tables +* Add `ReadableTable::first()` and `last()` which retrieve the first and last key-value pairs, respectively` +* Reduce lock contention for mixed read-write workloads +* Documentation improvements + +## 1.2.0 - 2023-09-24 +* Add `Builder::create_file()` which does the same thing as `create()` but + takes a `File` instead of a path +* Add `stats()` to tables which provides informational statistics on the table's storage +* Fix `WriteTransaction::stats()` to correctly count the storage used by multi-map tables +* Fix panics that could occur when operating on savepoints concurrently from multiple threads + on the same `WriteTransaction` +* Implement `Send` for `WriteTransaction` +* Change MSRV to 1.66 +* Performance optimizations + +## 1.1.0 - 2023-08-20 +* Fix panic when calling `compact()` on certain databases +* Fix panic when calling `compact()` when an ephemeral `Savepoint` existed +* Improve performance of `compact()` +* Relax lifetime requirements on arguments to `insert()` + +## 1.0.5 - 2023-07-16 +* Fix a rare panic when recovering a database file after a crash +* Minor performance improvement to write heavy workloads + +## 1.0.4 - 2023-07-01 +* Fix serious data corruption issue when calling `drain()` or `drain_filter()` on a `Table` that had + uncommitted data + +## 1.0.3 - 2023-06-30 +* Fix panic when re-opening databases of certain, small, sizes + +## 1.0.2 - 2023-06-29 +* Fix panic when recovering some databases after a forceful shutdown +* Fix panic when recovering databases with multimaps that have fixed width values after a forceful shutdown + +## 1.0.1 - 2023-06-26 +* Fix panic that could occur after an IO error when reopening a database +* Fix panic that could occur after an IO error when opening a table +* Improve error message when opening a table twice to include a more meaningful line number +* Performance improvements + +## 1.0.0 - 2023-06-16 +### Announcement +redb has reached its first stable release! The file format is now gauranteed to be backward compatible, +and the API is stable. I've run pretty extensive fuzz testing, but please report any bugs you encounter. + +The following features are complete: +* MVCC with a single `WriteTransaction` and multiple `ReadTransaction`s +* Zero-copy reads +* ACID semantics, including non-durable transactions which only sacrifice Durability +* Savepoints which allow the state of the database to be captured and restored later + +#### Changes from 0.22.0: +* Stabilize file format +* Improve performance of `restore_savepoint()` + +## 0.22.0 - 2023-06-12 +* Fix panic while repairing a database file after crash +* Fix rare panic in `restore_savepoint()` + +## 0.21.0 - 2023-06-09 +* Improve cache heuristic. This asymptotically improves performance on large databases. Benchmarks show 30% to 5x+ +* Fix rare crash that could occur under certain conditions when inserting values > 2GiB +* Fix crash when growing database beyond 4TiB +* Fix panic when repairing a database containing a multimap table with fixed width values +* Performance optimizations +* File format simplifications + +## 0.20.0 - 2023-05-30 +* Export `TransactionError` and `CommitError`. These were unintentionally private +* Implement `std::error::Error` for all error enums + +## 0.19.0 - 2023-05-29 +* Remove `Clone` bound from range argument type on `drain()` and `drain_filter()` +* File format changes to improve future extensibility + +## 0.18.0 - 2023-05-28 +* Improve errors to be more granular. `Error` has been split into multiple different `enum`s, which + can all be implicitly converted back to `Error` for convenience +* Rename `savepoint()` to `ephemeral_savepoint()` +* Add support for persistent savepoints. These persist across database restarts and must be explicitly + released +* Optimize `restore_savepoint()` to be ~30x faster +* Add experimental support for WASI. This requires nightly +* Implement `RedbKey` for `()` +* Fix some rare crash and data corruption bugs + +## 0.17.0 - 2023-05-09 +* Enforce a limit of 3GiB on keys & values +* Fix database corruption bug that could occur if a `Durability::None` commit was made, + followed by a durable commit and the durable commit crashed or encountered an I/O error during `commit()` +* Fix panic when re-openning a database file, when the process that last had it open had crashed +* Fix several bugs where an I/O error during `commit()` could cause a panic instead of returning an `Err` +* Change `length` argument to `insert_reserve()` to `u32` +* Change `Table::len()` to return `u64` +* Change width of most fields in `DatabaseStats` to `u64` +* Remove `K` type parameter from `AccessGuardMut` +* Add `Database::compact()` which compacts the database file +* Performance optimizations + +## 0.16.0 - 2023-04-28 +* Combine `Builder::set_read_cache_size()` and `Builder::set_write_cache_size()` into a single, + `Builder::set_cache_size()` setting +* Relax lifetime constraints on read methods on tables +* Optimizations to `Savepoint` + +## 0.15.0 - 2023-04-09 +* Add `Database::check_integrity()` to explicitly run repair process (it is still always run if needed on db open) +* Change `list_tables()` to return a `TableHandle` +* Change `delete_table()` to take a `TableHandle` +* Make `insert_reserve()` API signature type safe +* Change all iterators to return `Result` and propagate I/O errors +* Replace `WriteStrategy` with `Durability::Paranoid` +* Remove `Builder::set_initial_size()` +* Enable db file shrinking on Windows +* Performance optimizations + +## 0.14.0 - 2023-03-26 +* Remove `Builder::create_mmapped()` and `Builder::open_mmapped()`. The mmap backend has been removed + because it was infeasible to prove that it was sound. This makes the redb API entirely safe, + and the remaining `File` based backed is within a factor of ~2x on all workloads that I've benchmarked +* Make `Table` implement `Send`. It is now possible to insert into multiple `Table`s concurrently +* Expose `AccessGuardMut`, `Drain` and `DrainFilter` in the public API +* Rename `RangeIter` to `Range` +* Rename`MultimapRangeIter` to `MultimapRange` +* Rename `MultimapValueIter` to `MultimapValue` +* Performance optimizations + +## 0.13.0 - 2023-02-05 +* Fix a major data corruption issue that was introduced in version 0.12.0. It caused databases + greater than ~4GB to become irrecoverably corrupted due to an integer overflow in `PageNumber::address_range` + that was introduced by commit `b2c44a824d1ba69f526a1a75c56ae8484bae7248` +* Add `drain_filter()` to `Table` +* Make key and value type bounds more clear for tables + +## 0.12.1 - 2023-01-22 +* Fix `open()` on platforms with OS page size != 4KiB +* Relax lifetime requirements on argument to `range()` and `drain()` + +## 0.12.0 - 2023-01-21 +* Add `pop_first()` and `pop_last()` to `Table` +* Add `drain()` to `Table` +* Add support for `Option` as a value type +* Add support for user defined key and value types. Users must implement `RedbKey` and/or `RedbValue` +* Change `get()`, `insert()`, `remove()`...etc to take arguments of type `impl Borrow` +* Return `Error::UpgradeRequired` when opening a file with an outdated file format +* Improve support for 32bit platforms +* Performance optimizations + +## 0.11.0 - 2022-12-26 +* Remove `[u8]` and `str` type support. Use `&[u8]` and `&str` instead. +* Change `get()`, `range()` and several other methods to return `AccessGuard`. +* Rename `AccessGuard::to_value()` to `value()` +* Add a non-mmap based backend which is now the default. This makes `Database::create()` and + `Database::open()` safe, but has worse performance in some cases. The mmap backend is available + via `create_mmapped()`/`open_mmapped()`. There is no difference in the file format, so applications + can switch from one backend to the other. +* Better handling of fsync failures + +## 0.10.0 - 2022-11-23 +* Remove maximum database size argument from `create()`. Databases are now unbounded in size +* Reduce address space usage on Windows +* Remove `set_dynamic_growth()` +* Add `set_initial_size()` to `Builder` +* Optimize cleanup of deleted pages. This resolves a performance issue where openning a Database + or performing a small transaction, could be slow if the last committed transaction deleted a large + number of pages +* Remove `set_page_size()`. 4kB pages are always used now +* Add `iter()` method to `Table` and `MultimapTable` +* Fix various lifetime issues with type that had a lifetime, such as `&str` and `(&[u8], u64)` + +## 0.9.0 - 2022-11-05 +* Add support for dynamic file growth on Windows +* Add support for tuple types as keys and values +* Remove `Builder::set_region_size` +* Save lifetime from `Savepoint` +* Fix crash when using `create()` to open an existing database created with `WriteStrategy::TwoPhase` +* Fix rare crash when writing a mix of small and very large values into the same table +* Performance optimizations + +## 0.8.0 - 2022-10-18 +* Performance improvements for database files that are too large to fit in RAM +* Fix deadlock in concurrent calls to `savepoint()` and `restore_savepoint()` +* Fix crash if `restore_savepoint()` failed +* Move `savepoint()` and `restore_savepoint()` methods to `WriteTransaction` +* Implement `Iterator` for the types returned from `range()` and `remove_all()` + +## 0.7.0 - 2022-09-25 +* Add support for Windows +* Add `Database::set_write_strategy` which allows the `WriteStrategy` of the database to be changed after creation +* Make `Database::begin_write` block, instead of panic'ing, if there is another write already in progress +* Add `Database::savepoint` and `Database::restore_savepoint` which can be used to snapshot and rollback the database +* Rename `DatabaseBuilder` to `Builder` +* Performance optimizations for large databases + +## 0.6.1 - 2022-09-11 +* Fix crash when `Database::open()` was called on a database that had been created with `WriteStrategy::TwoPhase` +* Change default region size on 32bit platforms to 4GiB + +## 0.6.0 - 2022-09-10 +* Return `Err` instead of panic'ing when opening a database file with an incompatible file format version +* Many optimizations to the file format, and progress toward stabilizing it +* Fix race between read & write transactions, which could cause reads to return corrupted data +* Better document the different `WriteStrategy`s +* Fix panic when recovering a database that was uncleanly shutdown, which had been created with `WriteStrategy::Checksum` (which is the default) +* Fix panic when using `insert_reserve()` in certain cases + +## 0.5.0 - 2022-08-06 +* Optimize `MultimapTable` storage format to use `O(k * log(n_k) + v * log(n_v / n_k))` space instead of `O(k * log(n_k + n_v) + v * log(n_k + n_v))` space, + where k is the size of the stored keys, v is the size of the stored values, n_k is the number of stored keys, + n_v is the number of stored values +* Fix compilation errors for 32bit x86 targets +* Add support for the unit type, `()`, as a value +* Return an error when attempting to open the same database file for writing in multiple locations, concurrently +* More robust handling of fsync failures +* Change `MultimapTable::range` to return an iterator of key-value-collection pairs, instead of key-value pairs +* Automatically abort `WriteTransaction` on drop + +## 0.4.0 - 2022-07-26 +* Add single phase with checksum commit strategy. This is now the default and reduces commit latency by ~2x. For more details, + see the [design doc](docs/design.md#1-phase--checksum-durable-commits) and + [blog post](https://www.redb.org/post/2022/07/26/faster-commits-with-1pcc-instead-of-2pc/). The previous behavior is available + via `WriteStrategy::Throughput`, and can have better performance when writing a large number of bytes per transaction. + +## 0.3.1 - 2022-07-20 +* Fix a bug where re-opening a `Table` during a `WriteTransaction` lead to stale results being read + +## 0.3.0 - 2022-07-19 +* Fix a serious data corruption issue that caused many write operations to corrupt the database +* Make redb region size configurable +* Implement garbage collection of empty regions +* Fixes and optimizations to make the file format more efficient + +## 0.2.0 - 2022-06-10 +* Add information log messages which can be enabled with the `logging` feature +* Add support for `[u8; N]` type +* Optimize storage of fixed width types. The value length is no longer stored, which reduces storage space by ~50% for `u64`, + 2x for `u32`, and also improves performance. + +## 0.1.2 - 2022-05-08 +* Change `insert()` to return an `Option` with the previous value, instead of `()` + +## 0.1.1 - 2022-04-24 +* Improved documentation + +## 0.1.0 - 2022-04-23 +* Initial beta release diff --git a/v2/redb/CLAUDE.md b/v2/redb/CLAUDE.md new file mode 100644 index 000000000..0b3c692c5 --- /dev/null +++ b/v2/redb/CLAUDE.md @@ -0,0 +1 @@ +Read `AGENTS.md` for repository-specific agent instructions. diff --git a/v2/redb/Cargo.lock b/v2/redb/Cargo.lock new file mode 100644 index 000000000..a10bc0c5c --- /dev/null +++ b/v2/redb/Cargo.lock @@ -0,0 +1,832 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + +[[package]] +name = "bitflags" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "cc" +version = "1.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +dependencies = [ + "equivalent", + "hashbrown", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "redb" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef6a6d3a65ea334d6cdfb31fa2525c20184b7aa7bd1ad1e2e37502610d4609f" +dependencies = [ + "libc", +] + +[[package]] +name = "redb" +version = "4.1.0" +dependencies = [ + "bincode", + "chrono", + "libc", + "log", + "rand", + "redb 2.6.0", + "tempfile", + "uuid", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "uuid" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown", + "indexmap", + "semver", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] diff --git a/v2/redb/Cargo.toml b/v2/redb/Cargo.toml new file mode 100644 index 000000000..5cdedfe79 --- /dev/null +++ b/v2/redb/Cargo.toml @@ -0,0 +1,118 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2024" +rust-version = "1.89" +name = "redb" +version = "4.1.0" +authors = ["Christopher Berner "] +build = "build.rs" +exclude = ["fuzz/"] +autolib = false +autobins = false +autoexamples = false +autotests = false +autobenches = false +description = "Rust Embedded DataBase" +homepage = "https://www.redb.org" +readme = "README.md" +license = "MIT OR Apache-2.0" +repository = "https://github.com/cberner/redb" + +[features] +cache_metrics = [] +logging = ["dep:log"] + +[lib] +name = "redb" +path = "src/lib.rs" + +[[example]] +name = "bincode_keys" +path = "examples/bincode_keys.rs" + +[[example]] +name = "derive_value_impl" +path = "examples/derive_value_impl.rs" + +[[example]] +name = "int_keys" +path = "examples/int_keys.rs" + +[[example]] +name = "multithread" +path = "examples/multithread.rs" + +[[example]] +name = "special_values" +path = "examples/special_values.rs" + +[[test]] +name = "backward_compatibility" +path = "tests/backward_compatibility.rs" + +[[test]] +name = "basic_tests" +path = "tests/basic_tests.rs" + +[[test]] +name = "integration_tests" +path = "tests/integration_tests.rs" + +[[test]] +name = "multimap_tests" +path = "tests/multimap_tests.rs" + +[[test]] +name = "multithreading_tests" +path = "tests/multithreading_tests.rs" + +[dependencies.chrono_v0_4] +version = "0.4.41" +optional = true +package = "chrono" + +[dependencies.log] +version = "0.4.17" +optional = true + +[dependencies.uuid] +version = "1.17.0" +optional = true + +[dev-dependencies.bincode] +version = "2.0.1" + +[dev-dependencies.rand] +version = "0.10.1" + +[dev-dependencies.redb2_6] +version = "=2.6.0" +package = "redb" + +[dev-dependencies.tempfile] +version = "3.5.0" + +[dev-dependencies.uuid] +version = "1.17.0" +features = ["v4"] + +[target.'cfg(target_os = "wasi")'.dependencies.libc] +version = "0.2.174" + +[lints.clippy] +big_endian_bytes = "deny" +dbg_macro = "deny" +host_endian_bytes = "deny" + +[profile.bench] +debug = 2 diff --git a/v2/redb/Cargo.toml.orig b/v2/redb/Cargo.toml.orig new file mode 100644 index 000000000..ff67d0c54 --- /dev/null +++ b/v2/redb/Cargo.toml.orig @@ -0,0 +1,56 @@ +[package] +name = "redb" +description = "Rust Embedded DataBase" +readme = "README.md" +version = "4.1.0" +exclude = ["fuzz/"] +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +authors.workspace = true + +[workspace] +members = [".", "crates/redb-bench", "crates/redb-derive", "crates/redb-python"] +default-members = [".", "crates/redb-derive", "crates/redb-python"] + +[workspace.package] +edition = "2024" +rust-version = "1.89" +license = "MIT OR Apache-2.0" +homepage = "https://www.redb.org" +repository = "https://github.com/cberner/redb" +authors = ["Christopher Berner "] + +[dependencies] +log = { version = "0.4.17", optional = true } +chrono_v0_4 = { package = "chrono", version= "0.4.41", optional = true } +uuid = { version= "1.17.0", optional = true } + +[target.'cfg(target_os = "wasi")'.dependencies] +libc = "0.2.174" + +# Common test/bench dependencies +[dev-dependencies] +rand = "0.10.1" +tempfile = "3.5.0" +# for backwards compatibility testing - pin at 2.6.0 +redb2_6 = { version = "=2.6.0", package = "redb" } +bincode = "2.0.1" +uuid = { version= "1.17.0", features = ["v4"] } +redb-derive = { path = "./crates/redb-derive" } + +[features] +# Enables log messages +logging = ["dep:log"] +# Enable cache hit metrics +cache_metrics = [] + +[profile.bench] +debug = true + +[lints.clippy] +big_endian_bytes = "deny" +dbg_macro = "deny" +host_endian_bytes = "deny" diff --git a/v2/redb/Dockerfile.bench b/v2/redb/Dockerfile.bench new file mode 100644 index 000000000..17a6d894a --- /dev/null +++ b/v2/redb/Dockerfile.bench @@ -0,0 +1,15 @@ +FROM ubuntu:24.04 + +ARG DEBIAN_FRONTEND=noninteractive + +RUN apt update && apt install -y git build-essential curl libclang-dev + +ENV PATH=/root/.cargo/bin:$PATH + +ADD rust-toolchain /code/redb/rust-toolchain + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain=$(cat /code/redb/rust-toolchain) + +ADD . /code/redb/ + +RUN cd /code/redb && cargo bench --no-run -p redb-bench diff --git a/v2/redb/LICENSE-APACHE b/v2/redb/LICENSE-APACHE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/v2/redb/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/v2/redb/LICENSE-MIT b/v2/redb/LICENSE-MIT new file mode 100644 index 000000000..8a77f0eb8 --- /dev/null +++ b/v2/redb/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) 2021 Christopher Berner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/v2/redb/README.md b/v2/redb/README.md new file mode 100644 index 000000000..e1430fa4f --- /dev/null +++ b/v2/redb/README.md @@ -0,0 +1,92 @@ +# redb + +![CI](https://github.com/cberner/redb/actions/workflows/ci.yml/badge.svg) +[![Crates.io](https://img.shields.io/crates/v/redb.svg)](https://crates.io/crates/redb) +[![Documentation](https://docs.rs/redb/badge.svg)](https://docs.rs/redb) +[![License](https://img.shields.io/crates/l/redb)](https://crates.io/crates/redb) +[![dependency status](https://deps.rs/repo/github/cberner/redb/status.svg)](https://deps.rs/repo/github/cberner/redb) + +A simple, portable, high-performance, ACID, embedded key-value store. + +redb is written in pure Rust and is loosely inspired by [lmdb](http://www.lmdb.tech/doc/). Data is stored in a collection +of copy-on-write B-trees. For more details, see the [design doc](docs/design.md) + +```rust +use redb::{Database, Error, ReadableDatabase, TableDefinition}; + +const TABLE: TableDefinition<&str, u64> = TableDefinition::new("my_data"); + +fn main() -> Result<(), Error> { + let db = Database::create("my_db.redb")?; + let write_txn = db.begin_write()?; + { + let mut table = write_txn.open_table(TABLE)?; + table.insert("my_key", &123)?; + } + write_txn.commit()?; + + let read_txn = db.begin_read()?; + let table = read_txn.open_table(TABLE)?; + assert_eq!(table.get("my_key")?.unwrap().value(), 123); + + Ok(()) +} +``` + +## Status +Stable and maintained. + +The file format is stable, and a reasonable effort will be made to provide an upgrade path if there +are any future changes to it. + +## Features +* Zero-copy, thread-safe, `BTreeMap` based API +* Fully ACID-compliant transactions +* MVCC support for concurrent readers & writer, without blocking +* Crash-safe by default +* Savepoints and rollbacks + +## Development +To run all the tests and benchmarks a few extra dependencies are required: +* `cargo install cargo-deny --locked` +* `cargo install cargo-fuzz --locked` +* `apt install libclang-dev` + +## Benchmarks +redb has similar performance to other top embedded key-value stores such as lmdb and rocksdb + +| | redb | lmdb | rocksdb | sled | fjall | sqlite | +|---------------------------|-----------|------------|----------------|----------|-------------|------------| +| bulk load | 17063ms | **9232ms** | 13969ms | 24971ms | 18619ms | 15341ms | +| individual writes | **920ms** | 1598ms | 2432ms | 2701ms | 3488ms | 7040ms | +| batch writes | 1595ms | 942ms | 451ms | 853ms | **353ms** | 2625ms | +| len() | **0ms** | **0ms** | 749ms | 1573ms | 1181ms | 30ms | +| random reads | 1138ms | **637ms** | 2911ms | 1601ms | 2177ms | 4283ms | +| random reads | 934ms | **631ms** | 2884ms | 1592ms | 2357ms | 4281ms | +| random range reads | 1174ms | **565ms** | 2734ms | 1992ms | 2564ms | 8431ms | +| random range reads | 1173ms | **565ms** | 2742ms | 1993ms | 2690ms | 8449ms | +| random reads (4 threads) | 1390ms | **840ms** | 3995ms | 1913ms | 2606ms | 7000ms | +| random reads (8 threads) | 757ms | **427ms** | 2147ms | 1019ms | 1352ms | 8123ms | +| random reads (16 threads) | 652ms | **216ms** | 1478ms | 690ms | 963ms | 23022ms | +| random reads (32 threads) | 410ms | **125ms** | 1100ms | 444ms | 576ms | 26536ms | +| removals | 23297ms | 10435ms | 6900ms | 11088ms | **6004ms** | 10323ms | +| uncompacted size | 4.00 GiB | 2.61 GiB | **893.18 MiB** | 2.13 GiB | 1000.95 MiB | 1.09 GiB | +| compacted size | 1.69 GiB | 1.26 GiB | **454.71 MiB** | N/A | 1000.95 MiB | 556.85 MiB | + +Source code for benchmark [here](./crates/redb-bench/benches/lmdb_benchmark.rs). Results collected on a Ryzen 9950X3D with Samsung 9100 PRO NVMe. + +## License + +Licensed under either of + +* [Apache License, Version 2.0](LICENSE-APACHE) +* [MIT License](LICENSE-MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual licensed as above, without any additional terms or +conditions. diff --git a/v2/redb/build.rs b/v2/redb/build.rs new file mode 100644 index 000000000..e29ddc741 --- /dev/null +++ b/v2/redb/build.rs @@ -0,0 +1,10 @@ +fn main() { + println!("cargo:rustc-check-cfg=cfg(fuzzing)"); + + if std::env::var("CARGO_CFG_FUZZING").is_ok() + && std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") + { + println!("cargo:rustc-cdylib-link-arg=-undefined"); + println!("cargo:rustc-cdylib-link-arg=dynamic_lookup"); + } +} diff --git a/v2/redb/clippy.toml b/v2/redb/clippy.toml new file mode 100644 index 000000000..1e7e4127a --- /dev/null +++ b/v2/redb/clippy.toml @@ -0,0 +1,3 @@ +disallowed-methods = [ + "usize::to_le_bytes", +] diff --git a/v2/redb/deny.toml b/v2/redb/deny.toml new file mode 100644 index 000000000..3b5da4284 --- /dev/null +++ b/v2/redb/deny.toml @@ -0,0 +1,144 @@ +# This template contains all of the possible sections and their default values + +# Note that all fields that take a lint level have these possible values: +# * deny - An error will be produced and the check will fail +# * warn - A warning will be produced, but the check will not fail +# * allow - No warning or error will be produced, though in some cases a note +# will be + +# The values provided in this template are the default values that will be used +# when any section or field is not specified in your own configuration + +# This section is considered when running `cargo deny check advisories` +# More documentation for the advisories section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html +[advisories] +# The path where the advisory database is cloned/fetched into +db-path = "~/.cargo/advisory-db" +# The url of the advisory database to use +db-urls = ["https://github.com/rustsec/advisory-db"] +# The lint level for crates that have been yanked from their source registry +yanked = "warn" +# A list of advisory IDs to ignore. Note that ignored advisories will still +# output a note when they are encountered. +ignore = [ + #"RUSTSEC-0000-0000", +] +# Threshold for security vulnerabilities, any vulnerability with a CVSS score +# lower than the range specified will be ignored. Note that ignored advisories +# will still output a note when they are encountered. +# * None - CVSS Score 0.0 +# * Low - CVSS Score 0.1 - 3.9 +# * Medium - CVSS Score 4.0 - 6.9 +# * High - CVSS Score 7.0 - 8.9 +# * Critical - CVSS Score 9.0 - 10.0 +#severity-threshold = + +# This section is considered when running `cargo deny check licenses` +# More documentation for the licenses section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html +[licenses] +# List of explictly allowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.7 short identifier (+ optional exception)]. +allow = [ + "MIT", +# "BSD-2-Clause", +# "BSD-3-Clause", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", +] +# The confidence threshold for detecting a license from license text. +# The higher the value, the more closely the license text must be to the +# canonical license text of a valid SPDX license file. +# [possible values: any between 0.0 and 1.0]. +confidence-threshold = 0.8 +# Allow 1 or more licenses on a per-crate basis, so that particular licenses +# aren't accepted for every possible crate as with the normal allow list +exceptions = [ + # Each entry is the crate and version constraint, and its specific allow + # list + { allow = ["Unicode-3.0"], name = "unicode-ident", version = "*" }, # Used only by comfy-table in the benchmarks +# { allow = ["ISC"], name = "libloading", version = "*" }, # Used only by rocksdb in the benchmarks +] + +# Some crates don't have (easily) machine readable licensing information, +# adding a clarification entry for it allows you to manually specify the +# licensing information +#[[licenses.clarify]] +# The name of the crate the clarification applies to +#name = "ring" +# THe optional version constraint for the crate +#version = "*" +# The SPDX expression for the license requirements of the crate +#expression = "MIT AND ISC AND OpenSSL" +# One or more files in the crate's source used as the "source of truth" for +# the license expression. If the contents match, the clarification will be used +# when running the license check, otherwise the clarification will be ignored +# and the crate will be checked normally, which may produce warnings or errors +# depending on the rest of your configuration +#license-files = [ + # Each entry is a crate relative path, and the (opaque) hash of its contents + #{ path = "LICENSE", hash = 0xbd0eed23 } +#] + +[licenses.private] +# If true, ignores workspace crates that aren't published, or are only +# published to private registries +ignore = false +# One or more private registries that you might publish crates to, if a crate +# is only published to private registries, and ignore is true, the crate will +# not have its license(s) checked +registries = [ + #"https://sekretz.com/registry +] + +# This section is considered when running `cargo deny check bans`. +# More documentation about the 'bans' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html +[bans] +# Lint level for when multiple versions of the same crate are detected +multiple-versions = "warn" +# The graph highlighting used when creating dotgraphs for crates +# with multiple versions +# * lowest-version - The path to the lowest versioned duplicate is highlighted +# * simplest-path - The path to the version with the fewest edges is highlighted +# * all - Both lowest-version and simplest-path are used +highlight = "all" +# List of crates that are allowed. Use with care! +allow = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +# List of crates to deny +deny = [ + # Each entry the name of a crate and a version range. If version is + # not specified, all versions will be matched. + #{ name = "ansi_term", version = "=0.11.0" }, +] +# Certain crates/versions that will be skipped when doing duplicate detection. +skip = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +# Similarly to `skip` allows you to skip certain crates during duplicate +# detection. Unlike skip, it also includes the entire tree of transitive +# dependencies starting at the specified crate, up to a certain depth, which is +# by default infinite +skip-tree = [ + #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, +] + +# This section is considered when running `cargo deny check sources`. +# More documentation about the 'sources' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html +[sources] +# Lint level for what to happen when a crate from a crate registry that is not +# in the allow list is encountered +unknown-registry = "warn" +# Lint level for what to happen when a crate from a git repository that is not +# in the allow list is encountered +unknown-git = "warn" +# List of URLs for allowed crate registries. Defaults to the crates.io index +# if not specified. If it is specified but empty, no registries are allowed. +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +# List of URLs for allowed Git repositories +allow-git = [] diff --git a/v2/redb/docs/design.md b/v2/redb/docs/design.md new file mode 100644 index 000000000..691ec1d43 --- /dev/null +++ b/v2/redb/docs/design.md @@ -0,0 +1,493 @@ +# redb design + +-------------------------------------------------------------------------------------------------- + +redb is a simple, portable, high-performance, ACID, embedded key-value store. + +Each redb database contains a collection of tables, and supports a single writer and multiple +concurrent readers. redb uses MVCC to provide isolation, and provides a single isolation level: +serializable, in which all writes are applied sequentially. + +Each table is a key-value mapping providing an interface similar to `BTreeMap`. + +# File format +Logically, a redb database file consists of some metadata, and several B-trees: +* pending free tree: mapping from transaction ids to the list of pages they freed +* table tree: name -> table definition mapping of table names to their definitions +* data tree(s) (per one table): key -> value mapping for table + +Except for the database metadata, all other data structures are copy-on-write. + +All multi-byte integers are stored in little-endian order. + +The database file begins with the database header, and is followed by one or more regions. Each region contains a +header, and a data section which is split into many pages. These regions allow for efficient, dynamic, growth of the +database file. + +``` +<-------------------------------------------- 8 bytes -------------------------------------------> +========================================== Super header ========================================== +---------------------------------------- Header (64 bytes) --------------------------------------- +| magic number | +| magic con.| god byte | padding | page size | +| region header pages | region max data pages | +| number of full regions | data pages in trailing region | +| padding | +| padding | +| padding | +| padding | +------------------------------------ Commit slot 0 (128 bytes) ----------------------------------- +| version | user nn | sys nn | f-root nn | padding | +| user root page number | +| user root checksum | +| user root checksum (cont.) | +| user root length | +| system root page number | +| system root checksum | +| system root checksum (cont.) | +| system root length | +| padding | +| padding | +| padding | +| padding | +| transaction id | +| slot checksum | +| slot checksum (cont.) | +----------------------------------------- Commit slot 1 ------------------------------------------ +| Same layout as commit slot 0 | +---------------------------------- footer padding (192+ bytes) ----------------------------------- +================================================================================================== +| Region header (unused as of v3) | +-------------------------------------------------------------------------------------------------- +| Region data | +================================================================================================== +| ...more regions | +================================================================================================== +``` + +## Database super-header + +The database super-header is 512 bytes (rounded up to the nearest page) long, and consists of a header and two "commit slots". + +### Database header (64 bytes) +The database header contains several immutable fields, such as the database page size, region size, and a magic number. +Transaction data is stored in a double buffered field, and the primary copy is managed by updating a single byte that +controls which transaction pointer is the primary. + +* 9 bytes: magic number +* 1 byte: god byte +* 2 byte: padding +* 4 bytes: page size +* 4 bytes: region header pages +* 4 bytes: region max data pages +* 4 bytes: number of full regions +* 4 bytes: data pages in partial trailing region +* 32 bytes: padding to 64 bytes + +`magic number` must be set to the ASCII letters 'redb' followed by 0x1A, 0x0A, 0xA9, 0x0D, 0x0A. This sequence is +inspired by the PNG magic number. + +`god byte`, so named because this byte controls the state of the entire database, is a bitfield containing three flags: +* first bit: `primary_bit` flag which indicates whether transaction slot 0 or transaction slot 1 contains the latest commit. +* second bit: `recovery_required` flag, if set then the recovery process must be run when opening the database. This can be + a full repair, in which the region tracker and regional allocator states -- described below -- are reconstructed by walking + the btree from all active roots, or a quick-repair, in which the state is simply loaded from the allocator state table. +* third bit: `two_phase_commit` flag, which indicates whether the transaction in the primary slot was written using 2-phase + commit. If so, the primary slot is guaranteed to be valid, and repair won't look at the secondary slot. This flag is always + updated atomically along with the primary bit. + +redb relies on the fact that this is a single byte to perform atomic commits. + +`page size` is the size of a redb page in bytes + +`region header pages` is the number of pages in each region's header + +`region max data pages` is the maximum number of data pages in each region + +`number of full regions` stores the number of full regions in the database. This can change during a transaction if the database +grows or shrinks. This field is only valid when the database does not need recovery. Otherwise it must be recalculated from the file length and verified + +`pages in partial trailing region` all regions except the last must be full. This stores the number of pages in the last region. +This field is only valid when the database does not need recovery. Otherwise it must be recalculated from the file length and verified + +### Transaction slot 0 (128 bytes): +* 1 byte: file format version number +* 1 byte: boolean indicating that user root page is non-null +* 1 byte: boolean indicating that system root page is non-null +* 1 byte: boolean indicating that freed table root page is non-null +* 4 bytes: padding to 64-bit aligned +* 8 bytes: user root page +* 16 bytes: user root checksum +* 8 bytes: user root length +* 8 bytes: system root page +* 16 bytes: system root checksum +* 8 bytes: system root length +* 40 bytes: padding +* 8 bytes: last committed transaction id +* 16 bytes: slot checksum + +`version` the file format version of the database. This is stored in the transaction data, so that it can be atomically +changed during an upgrade. + +`user root page` is the page number of the root of the user table tree. + +`user root checksum` stores the XXH3_128bit checksum of the user root page, which in turn stores the checksum of its child pages. + +`user root length` is the number of tables in the user table tree. This field is new in file format v2. + +`system root page` is the page number of the root of the system table tree. + +`system root checksum` stores the XXH3_128bit checksum of the system root page, which in turn stores the checksum of its child pages. + +`system root length` is the number of tables in the system table tree. This field is new in file format v2. + +`slot checksum` is the XXH3_128bit checksum of all the preceding fields in the transaction slot. + +### Transaction slot 1 (128 bytes): +* Same layout as slot 0 + +### Region tracker +The region tracker is an array of `BtreeBitmap`s that tracks the page orders which are free in each region. +There are two different places it can be stored: on shutdown, it's written to a page in the data section of +a region, and when making a commit with quick-repair enabled, it's written to an entry in the allocator state +table. The former is valid only after a clean shutdown; the latter is usable even after a crash. +``` +<-------------------------------------------- 8 bytes -------------------------------------------> +================================================================================================== +| num_allocators | sub_allocator_len | +| BtreeBitmap data... | +================================================================================================== +``` + +A `BtreeBitmap` is a 64-way tree, where each node is a single value indicating whether any descendant is free. +These nodes are stored as single bits, packed into `u64` values. Every height of the tree is fully populated, except +the leaf layer. + +* 4 bytes: tree height +* 4 bytes (repeating): ending offset of layers. Does not include the root layer +* n bytes: tree data + +``` +<-------------------------------------------- 8 bytes -------------------------------------------> +================================================================================================== +| height | end offset... | +================================================================================================== +| Tree data | +================================================================================================== +``` + +## Region layout +* 1 byte: region format version +* 3 bytes: padding +* 4 bytes: length of the allocator state in bytes +* n bytes: the allocator state + +Each region consists of a header containing metadata -- primarily the allocation state of the region's pages -- and a +data section containing pages. Pages have a base size, which defaults to the OS page size, and are +variably sized in higher orders in power of 2 multiples of the base size. The format of pages is +described in the [following section](#b-tree-pages) + +``` +<-------------------------------------------- 8 bytes -------------------------------------------> +================================================================================================== +| version | padding | allocator state length | +-------------------------------------------------------------------------------------------------- +| Regional allocator state | +================================================================================================== +| Region pages | +================================================================================================== +``` + +### Regional allocator state +The regional allocator is a buddy allocator and allocates pages within the region. Its buddy allocator implementation relies +on the `BtreeBitmap` described above and the `U64GroupedBitmap`. The btree map is used to track the +pages which are free, and the grouped bitmap is used to track the order at which a even address +range has been allocated +* 1 byte: max order +* 3 byte: padding to 32bits aligned +* 4 bytes: number of pages +* 4 byte (repeating): end offset of order allocator free state +* 4 byte (repeating): end offset of order allocator allocated page state +* n bytes: free index data +* n bytes: allocated data + +Like the region tracker, there are two different places where the regional allocator state can be +stored. On shutdown, it's written to the region header as described above, and when making a commit +with quick-repair enabled, it's written to an entry in the allocator state table. The former is valid +only after a clean shutdown; the latter is usable even after a crash. + +``` +<-------------------------------------------- 8 bytes -------------------------------------------> +================================================================================================== +| max order | padding | number of pages | +-------------------------------------------------------------------------------------------------- +| Order end offsets... | +================================================================================================== +| Order allocator state... | +================================================================================================== +``` + +## B-tree pages + +Allocated pages may be of two types: b-tree branch pages, or b-tree leaf pages. The format of each is described below: + +### Branch page: +* 1 byte: type +* 1 byte: padding to 16bit alignment +* 2 bytes: num_keys (number of keys) +* 4 byte: padding to 64bit alignment +* repeating (num_keys + 1 times): +* * 16 bytes: child page checksum +* repeating (num_keys + 1 times): +* * 8 bytes: page number +* (optional) repeating (num_keys times): +* * 4 bytes: key end. Ending offset of the key, exclusive +* repeating (num_keys times): +* * n bytes: key data +``` +<-------------------------------------------- 8 bytes -------------------------------------------> +================================================================================================== +| type | padding | number of keys | padding | +-------------------------------------------------------------------------------------------------- +| child page checksum (repeated num_keys + 1 times) | +| | +-------------------------------------------------------------------------------------------------- +| child page number (repeated num_keys + 1 times) | +-------------------------------------------------------------------------------------------------- +| (optional) key end (repeated num_keys times) | alignment padding | +================================================================================================== +| Key data | +================================================================================================== +``` +`type` is `2` for a branch page + +`num_keys` specifies the number of key in the page + +`child page checksum` is an array of checksums of the child pages in the `page number` array + +`page number` is an array of child page numbers + +`key_end` is an array of ending offsets for the keys. It is optional, MUST NOT be stored for fixed width key types + +`alignment padding` optional padding so that the key data begins at a multiple of the key type's required alignment + +### Leaf page: +* 1 byte: type +* 1 byte: reserved (padding to 16bits aligned) +* 2 bytes: num_entries (number of pairs) +* (optional) repeating (num_entries times): +* * 4 bytes: key_end +* (optional) repeating (num_entries times): +* * 4 bytes: value_end +* repeating (num_entries times): +* * n bytes: key data +* repeating (num_entries times): +* * n bytes: value data +``` +<-------------------------------------------- 8 bytes -------------------------------------------> +================================================================================================== +| type | padding | number of entries | (optional) key end (repeated entries times) | +-------------------------------------------------------------------------------------------------- +| (optional) value end (repeated entries times) | (optional) key alignment padding | +================================================================================================== +| Key data | (optional) value alignment padding | +================================================================================================== +| Value data | +================================================================================================== +``` + +`type` is `1` for a leaf page + +`num_entries` specifies the number of key-value pairs in the leaf + +`key_end` is an array of ending offsets for the keys. It is optional, MUST NOT be stored for fixed width key types + +`value_end` is an array of ending offsets for the values. It is optional, MUST NOT be stored for fixed width value types + +`key alignment padding` optional padding so that the key data begins at a multiple of the key type's required alignment + +`value alignment padding` optional padding so that the value data begins at a multiple of the value type's required alignment + +# Commit strategies + +All data is checksumed when written, using a non-cryptographic Merkle tree with XXH3_128. This +allows a partially committed transaction to be detected and rolled back, after a crash. + +All redb transactions are atomic, and use one of the following commit strategies. + +## Non-durable commits +redb supports "non-durable" commits, meaning that there is no guarantee of durability. However, in the event of a crash +the database is still guaranteed to be consistent, and will return to either the last non-durable commit or the last +full commit that was made. +Non-durable commits are implemented with an in-memory flag that directs readers to read from the secondary page, +even though it is not yet promoted to the primary. +In the event of a crash, the database will simply rollback to the primary page and the allocator state can be safely +rebuilt via the normal repair process. +Note that freeing pages during a non-durable commit is not permitted, because it could be rolled back at anytime. + +## 1-phase + checksum durable commits (1PC+C) +A reduced latency commit strategy is used by default. A commit is performed with a single `fsync`. +First, all data and checksums are written, along with a monotonically incrementing transaction +id, then the primary is flipped and `fsync` called. If a crash occurs, we must verify that the primary has a larger +transaction id and that all of its checksums are valid. If this check fails, then the database will replace the partially +updated primary with the secondary. + +Below we give a brief correctness analysis of this 1-phase commit approach, and consider the different failure cases that could +occur during the fsync: + +1. If all or none of the data for the transaction is written to disk, this strategy clearly works +2. If some, but not all the data is written, then there are a few cases to consider: + 1. If the bit controlling the primary page is not updated, then the transaction has no effect and this approach is safe + 2. If it is updated, then we must verify that the transaction was completely written, or roll it back: + 1. If none of the transaction data was written, we will detect that the transaction id is older, and roll it back + 2. If some, but not all was written, then the checksum verification will fail, and it will be rolled back. + +## 2-phase durable commits (2PC) +A 2-phase commit strategy can also be used which mitigates a theoretical attack when handling malicious data +and when the attacker has high degree of control over the redb process (see below). +First, data is written to a new copy of the btree, second an `fsync` is performed, +finally the byte controlling which copy of the btree is the primary is flipped and a second `fsync` is performed. + +### Security of 1PC+C +Given that the 1PC+C commit strategy relies on a non-cyptographic checksum (XXH3) there is, at least in theory, a way to attack it. +Users who need to accept malicious input are encouraged to use 2PC instead. + +An attacker, could make a partially committed transaction appear as if it were completely committed. +The scenario would be something like: +``` +table.insert(malicious_key, malicious_value); +table.insert(good_key, good_value); +txn.commit(); +``` +and the attacker wants the transaction to appear as: +``` +table.insert(malicious_key, malicious_value); +txn.commit(); +``` +To do this they need to: +1) control the order in which pages are flushed to disk, so that the ones related to `good_key` are never written +2) introduce a crash during, or immediately before, the fsync() operation of the commit +3) ensure that the checksums for the partially written data are valid + +With complete control over the workload, or read access to the database file (3) is possible, since XXH3 is not collision resistant. +However, it requires the attacker to have knowledge of the database contents, because the input to the checksum includes +many other values (all the other keys in the b-tree root, along with their child node numbers) + +# MVCC (multi-version concurrency control) + +redb uses MVCC to isolate transactions from one another. This is implemented on top of the copy-on-write +b-tree data structure which underlies most of redb. Read transactions make a private copy of the root +of the b-tree, and are registered in the database so that no pages that root references are freed. +When a write transaction frees a page it is pushed into a queue, and only reused after all read transactions +that could reference it have completed. + +## Savepoints + +Savepoints and rollback are implemented on the same MVCC structures. When a savepoint is created it +registers as a read transaction to preserve a snapshot of the database. Additionally, it saves a copy +of the page allocator state -- this is about 64kB per 1GB of values (assuming 4kb pages). To rollback +the database to a savepoint the root of the b-tree is restored, and the snapshot of the page allocator +is diff'ed against the currently allocated pages to determine which have been allocated since the savepoint +was created. These pages are then queued to be freed, completing the rollback. + +Savepoints come in two varieties: +1. Ephemeral. These savepoints are immediately deallocated when they are dropped +2. Persistent. These savepoints are persisted in the database file and therefore across restarts. + They are stored in a table inside the system table tree. They must be explicitly deallocated. + +## Epoch based reclamation +As described above transactions and savepoints rely on epoch based reclamation of pages to ensure +that pages are only freed after they are no longer referenced. Care must be taken to ensure that a +page which is still referenced is never freed. The high level design is described below: + +### Definitions +* Transaction states: + * Uncommitted. An in-progress write transaction + * Committed. A committed write transaction + * Aborted. An aborted write transaction + * Orphaned. A read or write transaction which is no longer referenced + * Reclaimed. A write transaction which became orphaned and all of its pending free pages have + been reclaimed +* Dirty page: a page that is allocated and the transaction in which it was allocated has not committed yet +* Committed page: a page that is allocated and the transaction in which it was allocated has committed +* Pending free page: a page that is allocated that is no longer reachable from the data tree or system + tree roots, as of the associated transaction. These are stored in the freed tree. + +### Invariants +The following invariants must be maintained +* Committed pages are never modified in-place +* Committed pages may only be freed by transitioning into the "pending free page" state, and after + the associated transaction and all prior transactions have reached the Orphaned state +* Pages will only contain page pointers to pages in the same, or an earlier, transaction. This follows + from the fact that committed pages are never modified, and transaction ids being monotonically increasing +* Freed tree must only contain pages in the pending free page state + +### Key operations +#### Modification during write transactions +Write transactions uphold the required invariants through a very simple mechanism. All modifications +to commited pages are performed copy-on-write. That is a new page is allocated, modifications are +made in the new page, and a new b-tree is constructed to reference the new page. Dirty pages are +allowed to be modified in-place -- usually accomplished by freeing them. +The normal tables, system tables, and also the freed tree use this approach. +When a transaction aborts, all pages it allocated are immediately freed. + +#### Savepoint restore +Restoring a savepoint brings all normal tables back to the state they were in when the savepoint was +captured. The savepoint system tables are unaffected, and the freed trees need to be put into a +consistent state. +Restoring the normal tables is trivial, since the savepoint captures the data root. + +The freed table needs to be updated in two ways: +1) the restored data root from the savepoint must reference only committed pages, so we remove all + pending free pages from future transactions that are in the data freed tree. +2) all pages that become unreachable by restoring the savepoint data root must be added to the freed + table. This is done by adding all pages in the allocated pages table from the data tree to the + data freed tree. + +#### Database repair +To repair the database after an unclean shutdown we must: +1) Update the super header to reference the last fully committed transaction +2) Update the allocator state, so that it is consistent with all the database roots in the above + transaction + +If the last commit before the crash had quick-repair enabled, then these are both trivial. The +primary commit slot is guaranteed to be valid, because it was written using 2-phase commit, and +the corresponding allocator state is stored in the allocator state table. + +Otherwise, we need to perform a full repair: + +For (1), if the primary commit slot is invalid we switch to the secondary slot. + +For (2), we rebuild the allocator state by walking the following trees and marking all referenced +pages as allocated: +* data tree +* system tree +* freed tree, and all pending free pages contained within +All pages referenced by a savepoint must be contained in the above, because it is either: +a) referenced directly by the data, system, or freed tree -- i.e. it's a committed page +b) it is not referenced, in which case it is in the pending free state and is contained in the freed tree + +# Version changes +## v1 +Initial file format + +## v2 +Added a length field to both btrees and tables. This allows for constant time `len()` + +## v3 +* Removed the freed tree. Instead, these pages are stored in two system tables, one for the data tree +and one for the system tree. +* Added an "allocated pages" system table which tracks the pages allocated by each transaction when + a savepoint exists +* Removed the allocator state. Instead, the "quick repair" code path is used. + +# Assumptions about underlying media +redb is designed to be safe even in the event of power failure or on poorly behaved media. +Therefore, we make only a few assumptions about the guarantees provided by the underlying filesystem: +1. single byte writes are atomic: i.e., each byte will be written completely or not at all, + even in the event of a power failure +2. following an `fsync` operation writes are durable +3. ["powersafe overwrite"](https://www.sqlite.org/psow.html): When an application writes + a range of bytes in a file, no bytes outside of that range will change, + even if the write occurs just before a crash or power failure. sqlite makes this same + assumption, by default, in all modern versions. diff --git a/v2/redb/examples/bincode_keys.rs b/v2/redb/examples/bincode_keys.rs new file mode 100644 index 000000000..016b68550 --- /dev/null +++ b/v2/redb/examples/bincode_keys.rs @@ -0,0 +1,112 @@ +use std::any::type_name; +use std::cmp::Ordering; +use std::fmt::Debug; + +use bincode::{Decode, Encode, decode_from_slice, encode_to_vec}; +use redb::{Database, Error, Key, Range, ReadableDatabase, TableDefinition, TypeName, Value}; + +#[derive(Debug, Decode, Encode, PartialEq, Eq, PartialOrd, Ord)] +struct SomeKey { + foo: String, + bar: i32, +} + +#[derive(Debug, Decode, Encode, PartialEq)] +struct SomeValue { + foo: [f64; 3], + bar: bool, +} + +const TABLE: TableDefinition, Bincode> = + TableDefinition::new("my_data"); + +#[allow(clippy::result_large_err)] +fn main() -> Result<(), Error> { + let some_key = SomeKey { + foo: "hello world".to_string(), + bar: 42, + }; + let some_value = SomeValue { + foo: [1., 2., 3.], + bar: true, + }; + let lower = SomeKey { + foo: "a".to_string(), + bar: 42, + }; + let upper = SomeKey { + foo: "z".to_string(), + bar: 42, + }; + + let db = Database::create("bincode_keys.redb")?; + let write_txn = db.begin_write()?; + { + let mut table = write_txn.open_table(TABLE)?; + + table.insert(&some_key, &some_value).unwrap(); + } + write_txn.commit()?; + + let read_txn = db.begin_read()?; + let table = read_txn.open_table(TABLE)?; + + let mut iter: Range, Bincode> = table.range(lower..upper).unwrap(); + assert_eq!(iter.next().unwrap().unwrap().1.value(), some_value); + assert!(iter.next().is_none()); + + Ok(()) +} + +/// Wrapper type to handle keys and values using bincode serialization +#[derive(Debug)] +pub struct Bincode(pub T); + +impl Value for Bincode +where + T: Debug + Encode + Decode<()>, +{ + type SelfType<'a> + = T + where + Self: 'a; + + type AsBytes<'a> + = Vec + where + Self: 'a; + + fn fixed_width() -> Option { + None + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + decode_from_slice(data, bincode::config::standard()) + .unwrap() + .0 + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'a, + Self: 'b, + { + encode_to_vec(value, bincode::config::standard()).unwrap() + } + + fn type_name() -> TypeName { + TypeName::new(&format!("Bincode<{}>", type_name::())) + } +} + +impl Key for Bincode +where + T: Debug + Decode<()> + Encode + Ord, +{ + fn compare(data1: &[u8], data2: &[u8]) -> Ordering { + Self::from_bytes(data1).cmp(&Self::from_bytes(data2)) + } +} diff --git a/v2/redb/examples/derive_value_impl.rs b/v2/redb/examples/derive_value_impl.rs new file mode 100644 index 000000000..4477784a0 --- /dev/null +++ b/v2/redb/examples/derive_value_impl.rs @@ -0,0 +1,31 @@ +use redb::{Database, Error, ReadableDatabase, TableDefinition, Value}; + +use redb_derive::{Key, Value}; + +#[derive(Debug, Key, Value, PartialEq, Eq, PartialOrd, Ord, Clone)] +struct SomeKey { + foo: String, + bar: i32, +} + +const TABLE: TableDefinition = TableDefinition::new("my_data"); + +fn main() -> Result<(), Error> { + let db = Database::create("derived_keys.redb")?; + let key = SomeKey { + foo: "example".to_string(), + bar: 42, + }; + let write_txn = db.begin_write()?; + { + let mut table = write_txn.open_table(TABLE)?; + table.insert(key.clone(), 0)?; + } + write_txn.commit()?; + + let read_txn = db.begin_read()?; + let table = read_txn.open_table(TABLE)?; + assert_eq!(table.get(&key)?.unwrap().value(), 0); + + Ok(()) +} diff --git a/v2/redb/examples/int_keys.rs b/v2/redb/examples/int_keys.rs new file mode 100644 index 000000000..cc3dcf81d --- /dev/null +++ b/v2/redb/examples/int_keys.rs @@ -0,0 +1,20 @@ +use redb::{Database, Error, ReadableDatabase, TableDefinition}; + +const TABLE: TableDefinition = TableDefinition::new("my_data"); + +#[allow(clippy::result_large_err)] +fn main() -> Result<(), Error> { + let db = Database::create("int_keys.redb")?; + let write_txn = db.begin_write()?; + { + let mut table = write_txn.open_table(TABLE)?; + table.insert(0, 0)?; + } + write_txn.commit()?; + + let read_txn = db.begin_read()?; + let table = read_txn.open_table(TABLE)?; + assert_eq!(table.get(0)?.unwrap().value(), 0); + + Ok(()) +} diff --git a/v2/redb/examples/multithread.rs b/v2/redb/examples/multithread.rs new file mode 100644 index 000000000..f4f34e140 --- /dev/null +++ b/v2/redb/examples/multithread.rs @@ -0,0 +1,84 @@ +use redb::{Database, Error, TableDefinition}; +use redb::{ReadableDatabase, TableHandle}; +use std::time::Instant; +use std::{sync::Arc, time::Duration}; + +#[allow(clippy::result_large_err)] +fn main() -> Result<(), Error> { + let db = Database::create("my_db.redb")?; + let definition: TableDefinition<&str, u32> = TableDefinition::new("my_data"); + + let db = Arc::new(db); + // Seed the database with some information + let write_txn = db.begin_write()?; + { + let mut table = write_txn.open_table(definition)?; + table.insert(&0.to_string().as_str(), 0_u32)?; + // The resulting table should have a different "a" value each time this example is run + table.insert("a".to_string().as_str(), 0_u32)?; + } + write_txn.commit()?; + + let read_threads = 8; + let write_threads = 2; + let mut handles = Vec::with_capacity(read_threads + write_threads); + for i in 0..read_threads { + let db = db.clone(); + let h = std::thread::spawn(move || -> Result<(), Error> { + let start = Instant::now(); + while start.elapsed() < Duration::from_millis(100) { + let read_txn = db.begin_read()?; + // Print every (key, value) pair in the table + let table = read_txn.open_table(definition)?; + for (k, v) in table.range("0"..)?.flatten() { + println!("From read_thread #{}: {:?}, {:?}", i, k.value(), v.value()); + } + } + Ok(()) + }); + handles.push(h); + } + + for i in 0..write_threads { + let db = db.clone(); + let h = std::thread::spawn(move || -> Result<(), Error> { + let start = Instant::now(); + while start.elapsed() < Duration::from_millis(100) { + let write_txn = db.begin_write()?; + { + let mut table = write_txn.open_table(definition)?; + table.insert(&i.to_string().as_str(), i as u32)?; + // The resulting table should have a different "a" value each time this example is run + table.insert("a".to_string().as_str(), i as u32)?; + println!("Inserted data from write_thread #{i}"); + } + write_txn.commit()?; + } + Ok(()) + }); + handles.push(h); + } + + // See if there any errors were returned from the threads + for handle in handles { + if let Err(e) = handle.join() { + println!("{e:?}"); + } + } + + // Check that the `Database` has the table (and only the table) that we created + let read_txn = db.begin_read()?; + let tables = read_txn.list_tables()?; + for table in tables { + println!("Table: {}", table.name()); + let _d = TableDefinition::<&str, u32>::new(table.name()); + } + + // Print every (key, value) pair in the table + let table = read_txn.open_table(definition)?; + for (k, v) in table.range("0"..)?.flatten() { + println!("{:?}, {:?}", k.value(), v.value()); + } + + Ok(()) +} diff --git a/v2/redb/examples/special_values.rs b/v2/redb/examples/special_values.rs new file mode 100644 index 000000000..6b1b27205 --- /dev/null +++ b/v2/redb/examples/special_values.rs @@ -0,0 +1,123 @@ +use redb::{ + Database, Error, Key, ReadableTable, Table, TableDefinition, TableHandle, Value, + WriteTransaction, +}; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::marker::PhantomData; + +const TABLE: TableDefinition = TableDefinition::new("my_data"); + +struct SpecialValuesDb { + database: Database, + file: File, +} + +impl SpecialValuesDb { + fn new() -> Self { + SpecialValuesDb { + database: Database::create("index.redb").unwrap(), + file: OpenOptions::new() + .write(true) + .truncate(true) + .create(true) + .read(true) + .open("values.dat") + .unwrap(), + } + } + + fn begin_txn(&mut self) -> SpecialValuesTransaction<'_> { + SpecialValuesTransaction { + inner: self.database.begin_write().unwrap(), + file: &mut self.file, + } + } +} + +struct SpecialValuesTransaction<'db> { + inner: WriteTransaction, + file: &'db mut File, +} + +impl SpecialValuesTransaction<'_> { + fn open_table( + &mut self, + table: TableDefinition, + ) -> SpecialValuesTable<'_, K, V> { + let def: TableDefinition = TableDefinition::new(table.name()); + SpecialValuesTable { + inner: self.inner.open_table(def).unwrap(), + file: self.file, + _value_type: Default::default(), + } + } + + fn commit(self) { + self.file.sync_all().unwrap(); + self.inner.commit().unwrap(); + } +} + +struct SpecialValuesTable<'txn, K: Key + 'static, V: Value + 'static> { + inner: Table<'txn, K, (u64, u64)>, + file: &'txn mut File, + _value_type: PhantomData, +} + +impl SpecialValuesTable<'_, K, V> { + fn insert(&mut self, key: K::SelfType<'_>, value: V::SelfType<'_>) { + // Append to end of file + let offset = self.file.seek(SeekFrom::End(0)).unwrap(); + let value = V::as_bytes(&value); + self.file.write_all(value.as_ref()).unwrap(); + self.inner + .insert(key, (offset, value.as_ref().len() as u64)) + .unwrap(); + } + + fn get(&mut self, key: K::SelfType<'_>) -> ValueAccessor { + let (offset, length) = self.inner.get(key).unwrap().unwrap().value(); + self.file.seek(SeekFrom::Start(offset)).unwrap(); + let mut data = vec![0u8; length as usize]; + self.file.read_exact(data.as_mut_slice()).unwrap(); + ValueAccessor { + data, + _value_type: Default::default(), + } + } +} + +struct ValueAccessor { + data: Vec, + _value_type: PhantomData, +} + +impl ValueAccessor { + fn value(&self) -> V::SelfType<'_> { + V::from_bytes(&self.data) + } +} + +/// redb is not designed to support very large values, or values with special requirements (such as alignment or mutability). +/// There's a hard limit of slightly less than 4GiB per value, and performance is likely to be poor when mutating values above a few megabytes. +/// Additionally, because redb is copy-on-write, mutating a value in-place is not possible, and therefore mutating large values is slow. +/// Storing values with alignment requirements is also not supported. +/// +/// This example demonstrates one way to handle such values, via a sidecar file. +#[allow(clippy::result_large_err)] +fn main() -> Result<(), Error> { + let mut db = SpecialValuesDb::new(); + let mut txn = db.begin_txn(); + { + let mut table = txn.open_table(TABLE); + table.insert(0, "hello world"); + } + txn.commit(); + + let mut txn = db.begin_txn(); + let mut table = txn.open_table(TABLE); + assert_eq!(table.get(0).value(), "hello world"); + + Ok(()) +} diff --git a/v2/redb/justfile b/v2/redb/justfile new file mode 100644 index 000000000..2eae69d48 --- /dev/null +++ b/v2/redb/justfile @@ -0,0 +1,84 @@ +build: pre + cargo build --all-targets --all-features + cargo doc + +build_all: pre_all + cargo build --all --all-targets --all-features + cargo doc --all + +pre: + cargo deny --workspace --all-features check licenses + cargo fmt --all -- --check + cargo clippy --all-targets --all-features + +pre_all: + cargo deny --workspace --all-features check licenses + cargo fmt --all -- --check + cargo clippy --all --all-targets --all-features + +release: pre + cargo build --release + +flamegraph: + cargo flamegraph -p redb-bench --bench redb_benchmark + firefox ./flamegraph.svg + +publish_py: test_py + docker pull quay.io/pypa/manylinux2014_x86_64 + MATURIN_PYPI_TOKEN=$(cat ~/.pypi/redb_token) docker run -it --rm -e "MATURIN_PYPI_TOKEN" -v `pwd`:/redb-ro:ro quay.io/pypa/manylinux2014_x86_64 /redb-ro/crates/redb-python/py_publish.sh + +test_py: install_py + python3 -m unittest discover --start-directory=./crates/redb-python + +install_py: pre + maturin develop --manifest-path=./crates/redb-python/Cargo.toml + +test: pre + RUST_BACKTRACE=1 cargo test --all-features + +test_all: build_all + RUST_BACKTRACE=1 cargo test --all --all-features + +test_wasi: + rustup install nightly-2025-07-26 --target wasm32-wasip1-threads + # Uses cargo pkgid because "redb" is ambiguous with the test dependency on an old version of redb + cargo +nightly-2025-07-26 test -p $(cargo pkgid) --target=wasm32-wasip1-threads -- --nocapture + cargo +nightly-2025-07-26 test -p redb-derive --target=wasm32-wasip1-threads -- --nocapture + +bench bench='redb_benchmark': pre + cargo bench -p redb-bench --bench {{bench}} + +build_bench_container: + docker build -t redb-bench:latest -f Dockerfile.bench . + +bench_containerized bench='lmdb_benchmark': build_bench_container + # Exec the binary directly, because at low memory limits there may not be enough to invoke cargo & rustc + docker run --rm -it --memory=4g redb-bench:latest bash -c "cd /code/redb && ./target/release/deps/{{bench}}-*" + +watch +args='test': + cargo watch --clear --exec "{{args}}" + +fuzz: pre + cargo fuzz run --sanitizer=none fuzz_redb -- -max_len=10000 + +fuzz_cmin: + cargo fuzz cmin --sanitizer=none fuzz_redb -- -max_len=10000 + +fuzz_ci: pre_all + cargo fuzz run --sanitizer=none fuzz_redb -- -max_len=10000 -max_total_time=60 + +fuzz_coverage: pre + #!/usr/bin/env bash + set -euxo pipefail + rustup component add llvm-tools-preview + RUST_SYSROOT=`cargo rustc -- --print sysroot 2>/dev/null` + LLVM_COV=`find $RUST_SYSROOT -name llvm-cov` + echo $LLVM_COV + cargo fuzz coverage --sanitizer=none fuzz_redb + $LLVM_COV show target/*/coverage/*/release/fuzz_redb --format html \ + -instr-profile=fuzz/coverage/fuzz_redb/coverage.profdata \ + -ignore-filename-regex='.*(cargo/registry|redb/fuzz|rustc).*' > fuzz/coverage/coverage_report.html + $LLVM_COV report target/*/coverage/*/release/fuzz_redb \ + -instr-profile=fuzz/coverage/fuzz_redb/coverage.profdata \ + -ignore-filename-regex='.*(cargo/registry|redb/fuzz|rustc).*' + firefox ./fuzz/coverage/coverage_report.html diff --git a/v2/redb/rust-toolchain b/v2/redb/rust-toolchain new file mode 100644 index 000000000..93132999c --- /dev/null +++ b/v2/redb/rust-toolchain @@ -0,0 +1 @@ +1.89 diff --git a/v2/redb/rustfmt.toml b/v2/redb/rustfmt.toml new file mode 100644 index 000000000..758d4179d --- /dev/null +++ b/v2/redb/rustfmt.toml @@ -0,0 +1 @@ +max_width = 100 diff --git a/v2/redb/src/backends.rs b/v2/redb/src/backends.rs new file mode 100644 index 000000000..d6b6a2891 --- /dev/null +++ b/v2/redb/src/backends.rs @@ -0,0 +1,2 @@ +pub use crate::tree_store::InMemoryBackend; +pub use crate::tree_store::file_backend::FileBackend; diff --git a/v2/redb/src/complex_types.rs b/v2/redb/src/complex_types.rs new file mode 100644 index 000000000..cbceec674 --- /dev/null +++ b/v2/redb/src/complex_types.rs @@ -0,0 +1,93 @@ +use crate::types::{TypeName, Value}; + +// Encode len as a varint and store it at the end of output +pub(super) fn encode_varint_len(len: usize, output: &mut Vec) { + if len < 254 { + output.push(len.try_into().unwrap()); + } else if len <= u16::MAX.into() { + let u16_len: u16 = len.try_into().unwrap(); + output.push(254); + output.extend_from_slice(&u16_len.to_le_bytes()); + } else { + let u32_len: u32 = len.try_into().unwrap(); + output.push(255); + output.extend_from_slice(&u32_len.to_le_bytes()); + } +} + +// Decode a variable length int starting at the beginning of data +// Returns (decoded length, length consumed of `data`) +pub(super) fn decode_varint_len(data: &[u8]) -> (usize, usize) { + match data[0] { + 0..=253 => (data[0] as usize, 1), + 254 => ( + u16::from_le_bytes(data[1..3].try_into().unwrap()) as usize, + 3, + ), + 255 => ( + u32::from_le_bytes(data[1..5].try_into().unwrap()) as usize, + 5, + ), + } +} + +impl Value for Vec { + type SelfType<'a> + = Vec> + where + Self: 'a; + type AsBytes<'a> + = Vec + where + Self: 'a; + + fn fixed_width() -> Option { + None + } + + fn from_bytes<'a>(data: &'a [u8]) -> Vec> + where + Self: 'a, + { + let (elements, mut offset) = decode_varint_len(data); + let mut result = Vec::with_capacity(elements); + for _ in 0..elements { + let element_len = if let Some(len) = T::fixed_width() { + len + } else { + let (len, consumed) = decode_varint_len(&data[offset..]); + offset += consumed; + len + }; + result.push(T::from_bytes(&data[offset..(offset + element_len)])); + offset += element_len; + } + assert_eq!(offset, data.len()); + result + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Vec>) -> Vec + where + Self: 'b, + { + let mut result = if let Some(width) = T::fixed_width() { + Vec::with_capacity(value.len() * width + 5) + } else { + Vec::with_capacity(value.len() * 2 + 5) + }; + encode_varint_len(value.len(), &mut result); + + for element in value { + let serialized = T::as_bytes(element); + if T::fixed_width().is_none() { + encode_varint_len(serialized.as_ref().len(), &mut result); + } + result.extend_from_slice(serialized.as_ref()); + } + result + } + + fn type_name() -> TypeName { + TypeName::internal(&format!("Vec<{}>", T::type_name().name())) + } +} diff --git a/v2/redb/src/db.rs b/v2/redb/src/db.rs new file mode 100644 index 000000000..9854b219c --- /dev/null +++ b/v2/redb/src/db.rs @@ -0,0 +1,1696 @@ +use crate::transaction_tracker::{TransactionId, TransactionTracker}; +use crate::tree_store::{ + BtreeHeader, InternalTableDefinition, PAGE_SIZE, PageHint, PageNumber, ReadOnlyBackend, + ShrinkPolicy, TableTree, TableType, TransactionalMemory, +}; +use crate::types::{Key, Value}; +use crate::{ + CompactionError, DatabaseError, Error, ReadOnlyTable, SavepointError, StorageError, TableError, +}; +use crate::{ReadTransaction, Result, WriteTransaction}; +use std::fmt::{Debug, Display, Formatter}; + +use std::fs::{File, OpenOptions}; +use std::marker::PhantomData; +use std::path::Path; +use std::sync::Arc; +use std::{io, thread}; + +use crate::error::TransactionError; +use crate::sealed::Sealed; +use crate::transactions::{ + ALLOCATOR_STATE_TABLE_NAME, AllocatorStateKey, AllocatorStateTree, DATA_ALLOCATED_TABLE, + DATA_FREED_TABLE, PageList, SYSTEM_FREED_TABLE, SystemTableDefinition, + TransactionIdWithPagination, +}; +use crate::tree_store::file_backend::FileBackend; +#[cfg(feature = "logging")] +use log::{debug, info, warn}; + +#[allow(clippy::len_without_is_empty)] +/// Implements persistent storage for a database. +pub trait StorageBackend: 'static + Debug + Send + Sync { + /// Gets the current length of the storage. + fn len(&self) -> std::result::Result; + + /// Reads the specified array of bytes from the storage. + /// + /// If `out.len()` + `offset` exceeds the length of the storage an appropriate `Error` must be returned. + fn read(&self, offset: u64, out: &mut [u8]) -> std::result::Result<(), io::Error>; + + /// Sets the length of the storage. + /// + /// New positions in the storage must be initialized to zero. + fn set_len(&self, len: u64) -> std::result::Result<(), io::Error>; + + /// Syncs all buffered data with the persistent storage. + fn sync_data(&self) -> std::result::Result<(), io::Error>; + + /// Writes the specified array to the storage. + fn write(&self, offset: u64, data: &[u8]) -> std::result::Result<(), io::Error>; + + /// Release any resources held by the backend + /// + /// Note: redb will not access the backend after calling this method and will call it exactly + /// once when the [`Database`] is dropped + fn close(&self) -> std::result::Result<(), io::Error> { + Ok(()) + } +} + +pub trait TableHandle: Sealed { + // Returns the name of the table + fn name(&self) -> &str; +} + +#[derive(Clone)] +pub struct UntypedTableHandle { + name: String, +} + +impl UntypedTableHandle { + pub(crate) fn new(name: String) -> Self { + Self { name } + } +} + +impl TableHandle for UntypedTableHandle { + fn name(&self) -> &str { + &self.name + } +} + +impl Sealed for UntypedTableHandle {} + +pub trait MultimapTableHandle: Sealed { + // Returns the name of the multimap table + fn name(&self) -> &str; +} + +#[derive(Clone)] +pub struct UntypedMultimapTableHandle { + name: String, +} + +impl UntypedMultimapTableHandle { + pub(crate) fn new(name: String) -> Self { + Self { name } + } +} + +impl MultimapTableHandle for UntypedMultimapTableHandle { + fn name(&self) -> &str { + &self.name + } +} + +impl Sealed for UntypedMultimapTableHandle {} + +/// Defines the name and types of a table +/// +/// A [`TableDefinition`] should be opened for use by calling [`ReadTransaction::open_table`] or [`WriteTransaction::open_table`] +/// +/// Note that the lifetime of the `K` and `V` type parameters does not impact the lifetimes of the data +/// that is stored or retreived from the table +pub struct TableDefinition<'a, K: Key + 'static, V: Value + 'static> { + name: &'a str, + _key_type: PhantomData, + _value_type: PhantomData, +} + +impl<'a, K: Key + 'static, V: Value + 'static> TableDefinition<'a, K, V> { + /// Construct a new table with given `name` + /// + /// # Panics + /// + /// Panics if `name` is empty. When `name` is a non-empty string literal + /// this is checked at compile time, but callers that build the name at + /// runtime are responsible for ensuring it is non-empty. + pub const fn new(name: &'a str) -> Self { + assert!(!name.is_empty()); + Self { + name, + _key_type: PhantomData, + _value_type: PhantomData, + } + } +} + +impl TableHandle for TableDefinition<'_, K, V> { + fn name(&self) -> &str { + self.name + } +} + +impl Sealed for TableDefinition<'_, K, V> {} + +impl Clone for TableDefinition<'_, K, V> { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for TableDefinition<'_, K, V> {} + +impl Display for TableDefinition<'_, K, V> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}<{}, {}>", + self.name, + K::type_name().name(), + V::type_name().name() + ) + } +} + +/// Defines the name and types of a multimap table +/// +/// A [`MultimapTableDefinition`] should be opened for use by calling [`ReadTransaction::open_multimap_table`] or [`WriteTransaction::open_multimap_table`] +/// +/// [Multimap tables](https://en.wikipedia.org/wiki/Multimap) may have multiple values associated with each key +/// +/// Note that the lifetime of the `K` and `V` type parameters does not impact the lifetimes of the data +/// that is stored or retreived from the table +pub struct MultimapTableDefinition<'a, K: Key + 'static, V: Key + 'static> { + name: &'a str, + _key_type: PhantomData, + _value_type: PhantomData, +} + +impl<'a, K: Key + 'static, V: Key + 'static> MultimapTableDefinition<'a, K, V> { + /// Construct a new multimap table with given `name` + /// + /// # Panics + /// + /// Panics if `name` is empty. When `name` is a non-empty string literal + /// this is checked at compile time, but callers that build the name at + /// runtime are responsible for ensuring it is non-empty. + pub const fn new(name: &'a str) -> Self { + assert!(!name.is_empty()); + Self { + name, + _key_type: PhantomData, + _value_type: PhantomData, + } + } +} + +impl MultimapTableHandle for MultimapTableDefinition<'_, K, V> { + fn name(&self) -> &str { + self.name + } +} + +impl Sealed for MultimapTableDefinition<'_, K, V> {} + +impl Clone for MultimapTableDefinition<'_, K, V> { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for MultimapTableDefinition<'_, K, V> {} + +impl Display for MultimapTableDefinition<'_, K, V> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}<{}, {}>", + self.name, + K::type_name().name(), + V::type_name().name() + ) + } +} + +/// Information regarding the usage of the in-memory cache +/// +/// Note: these metrics are only collected when the "`cache_metrics`" feature is enabled +#[derive(Debug)] +pub struct CacheStats { + pub(crate) evictions: u64, + pub(crate) read_hits: u64, + pub(crate) read_misses: u64, + pub(crate) write_hits: u64, + pub(crate) write_misses: u64, + pub(crate) used_bytes: usize, +} + +impl CacheStats { + /// Number of times that data has been evicted, due to the cache being full + /// + /// To increase the cache size use [`Builder::set_cache_size`] + pub fn evictions(&self) -> u64 { + self.evictions + } + + /// Number of times that unmodified data has been read from the cache + pub fn read_hits(&self) -> u64 { + self.read_hits + } + + /// Number of times that unmodified data was not in the cache and was read from storage + pub fn read_misses(&self) -> u64 { + self.read_misses + } + + /// Number of times that data modified in a transaction has been read from the cache + pub fn write_hits(&self) -> u64 { + self.write_hits + } + + /// Number of times that data modified in a transaction was not in the cache and was read from storage + pub fn write_misses(&self) -> u64 { + self.write_misses + } + + /// Number of bytes in the cache + pub fn used_bytes(&self) -> usize { + self.used_bytes + } +} + +pub(crate) struct TransactionGuard { + transaction_tracker: Option>, + transaction_id: Option, + write_transaction: bool, +} + +impl TransactionGuard { + pub(crate) fn new_read( + transaction_id: TransactionId, + tracker: Arc, + ) -> Self { + Self { + transaction_tracker: Some(tracker), + transaction_id: Some(transaction_id), + write_transaction: false, + } + } + + pub(crate) fn new_write( + transaction_id: TransactionId, + tracker: Arc, + ) -> Self { + Self { + transaction_tracker: Some(tracker), + transaction_id: Some(transaction_id), + write_transaction: true, + } + } + + // TODO: remove this hack + pub(crate) fn fake() -> Self { + Self { + transaction_tracker: None, + transaction_id: None, + write_transaction: false, + } + } + + pub(crate) fn id(&self) -> TransactionId { + self.transaction_id.unwrap() + } + + pub(crate) fn leak(mut self) -> TransactionId { + self.transaction_id.take().unwrap() + } +} + +impl Drop for TransactionGuard { + fn drop(&mut self) { + if self.transaction_tracker.is_none() { + return; + } + if let Some(transaction_id) = self.transaction_id { + if self.write_transaction { + self.transaction_tracker + .as_ref() + .unwrap() + .end_write_transaction(transaction_id); + } else { + self.transaction_tracker + .as_ref() + .unwrap() + .deallocate_read_transaction(transaction_id); + } + } + } +} + +pub trait ReadableDatabase { + /// Begins a read transaction + /// + /// Captures a snapshot of the database, so that only data committed before calling this method + /// is visible in the transaction + /// + /// Returns a [`ReadTransaction`] which may be used to read from the database. Read transactions + /// may exist concurrently with writes + fn begin_read(&self) -> Result; + + /// Information regarding the usage of the in-memory cache + /// + /// Note: these metrics are only collected when the "`cache_metrics`" feature is enabled + fn cache_stats(&self) -> CacheStats; +} + +/// A redb database opened in read-only mode +/// +/// Use [`Self::begin_read`] to get a [`ReadTransaction`] object that can be used to read from the database +/// +/// Multiple processes may open a [`ReadOnlyDatabase`], but it may not be opened concurrently +/// with a [`Database`]. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ```rust +/// use redb::*; +/// # use tempfile::NamedTempFile; +/// const TABLE: TableDefinition = TableDefinition::new("my_data"); +/// +/// # fn main() -> Result<(), Error> { +/// # #[cfg(not(target_os = "wasi"))] +/// # let tmpfile = NamedTempFile::new().unwrap(); +/// # #[cfg(target_os = "wasi")] +/// # let tmpfile = NamedTempFile::new_in("/tmp").unwrap(); +/// # let filename = tmpfile.path(); +/// let db = Database::create(filename)?; +/// let txn = db.begin_write()?; +/// { +/// let mut table = txn.open_table(TABLE)?; +/// table.insert(&0, &0)?; +/// } +/// txn.commit()?; +/// drop(db); +/// +/// let db = ReadOnlyDatabase::open(filename)?; +/// let txn = db.begin_read()?; +/// { +/// let mut table = txn.open_table(TABLE)?; +/// println!("{}", table.get(&0)?.unwrap().value()); +/// } +/// # Ok(()) +/// # } +/// ``` +pub struct ReadOnlyDatabase { + mem: Arc, + transaction_tracker: Arc, +} + +impl ReadableDatabase for ReadOnlyDatabase { + fn begin_read(&self) -> Result { + let id = self + .transaction_tracker + .register_read_transaction(&self.mem)?; + #[cfg(feature = "logging")] + debug!("Beginning read transaction id={id:?}"); + + let guard = TransactionGuard::new_read(id, self.transaction_tracker.clone()); + + ReadTransaction::new(self.mem.clone(), guard) + } + + fn cache_stats(&self) -> CacheStats { + self.mem.cache_stats() + } +} + +impl ReadOnlyDatabase { + /// Opens an existing redb database. + pub fn open(path: impl AsRef) -> Result { + Builder::new().open_read_only(path) + } + + fn new( + file: Box, + page_size: usize, + region_size: Option, + cache_size: usize, + ) -> Result { + #[cfg(feature = "logging")] + let file_path = format!("{:?}", &file); + #[cfg(feature = "logging")] + info!("Opening database in read-only {:?}", &file_path); + let mem = TransactionalMemory::new( + Box::new(ReadOnlyBackend::new(file)), + false, + page_size, + region_size, + cache_size, + true, + )?; + let mem = Arc::new(mem); + // If the last transaction used 2-phase commit and updated the allocator state table, then + // we can just load the allocator state from there. Otherwise, we need a full repair + if let Some(tree) = Database::get_allocator_state_table(&mem)? { + mem.load_allocator_state(&tree)?; + } else { + #[cfg(feature = "logging")] + warn!( + "Database {:?} not shutdown cleanly. Repair required", + &file_path + ); + return Err(DatabaseError::RepairAborted); + } + + let next_transaction_id = mem.get_last_committed_transaction_id()?.next(); + let db = Self { + mem, + transaction_tracker: Arc::new(TransactionTracker::new(next_transaction_id)), + }; + + Ok(db) + } +} + +/// Opened redb database file +/// +/// Use [`Self::begin_read`] to get a [`ReadTransaction`] object that can be used to read from the database +/// Use [`Self::begin_write`] to get a [`WriteTransaction`] object that can be used to read or write to the database +/// +/// Multiple reads may be performed concurrently, with each other, and with writes. Only a single write +/// may be in progress at a time. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ```rust +/// use redb::*; +/// # use tempfile::NamedTempFile; +/// const TABLE: TableDefinition = TableDefinition::new("my_data"); +/// +/// # fn main() -> Result<(), Error> { +/// # #[cfg(not(target_os = "wasi"))] +/// # let tmpfile = NamedTempFile::new().unwrap(); +/// # #[cfg(target_os = "wasi")] +/// # let tmpfile = NamedTempFile::new_in("/tmp").unwrap(); +/// # let filename = tmpfile.path(); +/// let db = Database::create(filename)?; +/// let write_txn = db.begin_write()?; +/// { +/// let mut table = write_txn.open_table(TABLE)?; +/// table.insert(&0, &0)?; +/// } +/// write_txn.commit()?; +/// # Ok(()) +/// # } +/// ``` +pub struct Database { + mem: Arc, + transaction_tracker: Arc, +} + +impl ReadableDatabase for Database { + fn begin_read(&self) -> Result { + let guard = self.allocate_read_transaction()?; + #[cfg(feature = "logging")] + debug!("Beginning read transaction id={:?}", guard.id()); + ReadTransaction::new(self.get_memory(), guard) + } + + fn cache_stats(&self) -> CacheStats { + self.mem.cache_stats() + } +} + +impl Database { + /// Opens the specified file as a redb database. + /// * if the file does not exist, or is an empty file, a new database will be initialized in it + /// * if the file is a valid redb database, it will be opened + /// * otherwise this function will return an error + pub fn create(path: impl AsRef) -> Result { + Self::builder().create(path) + } + + /// Opens an existing redb database. + pub fn open(path: impl AsRef) -> Result { + Self::builder().open(path) + } + + pub(crate) fn get_memory(&self) -> Arc { + self.mem.clone() + } + + pub(crate) fn verify_primary_checksums(mem: Arc) -> Result { + let table_tree = TableTree::new( + mem.get_data_root(), + PageHint::None, + Arc::new(TransactionGuard::fake()), + mem.clone(), + )?; + if !table_tree.verify_checksums()? { + return Ok(false); + } + let system_table_tree = TableTree::new( + mem.get_system_root(), + PageHint::None, + Arc::new(TransactionGuard::fake()), + mem.clone(), + )?; + if !system_table_tree.verify_checksums()? { + return Ok(false); + } + + Ok(true) + } + + /// Force a check of the integrity of the database file, and repair it if possible. + /// + /// Note: Calling this function is unnecessary during normal operation. redb will automatically + /// detect and recover from crashes, power loss, and other unclean shutdowns. This function is + /// quite slow and should only be used when you suspect the database file may have been modified + /// externally to redb, or that a redb bug may have left the database in a corrupted state. + /// + /// Returns `Ok(true)` if the database passed integrity checks; `Ok(false)` if it failed but was repaired, + /// and `Err(Corrupted)` if the check failed and the file could not be repaired. + /// + /// Returns [`DatabaseError::TransactionInProgress`] if any read or write transaction is still + /// alive when this method is called. + pub fn check_integrity(&mut self) -> Result { + let allocator_hash = self.mem.allocator_hash(); + let mem = Arc::get_mut(&mut self.mem).ok_or(DatabaseError::TransactionInProgress)?; + let mut was_clean = mem.clear_cache_and_reload()?; + + let old_roots = [self.mem.get_data_root(), self.mem.get_system_root()]; + + let new_roots = Self::do_repair(&mut self.mem, &|_| {}).map_err(|err| match err { + DatabaseError::Storage(storage_err) => storage_err, + _ => unreachable!(), + })?; + + if old_roots != new_roots || allocator_hash != self.mem.allocator_hash() { + was_clean = false; + } + + if !was_clean { + let next_transaction_id = self.mem.get_last_committed_transaction_id()?.next(); + let [data_root, system_root] = new_roots; + self.mem.commit( + data_root, + system_root, + next_transaction_id, + true, + ShrinkPolicy::Never, + )?; + } + + self.mem.begin_writable()?; + + Ok(was_clean) + } + + /// Compacts the database file + /// + /// Returns `true` if compaction was performed, and `false` if no futher compaction was possible + pub fn compact(&mut self) -> Result { + if self + .transaction_tracker + .oldest_live_read_transaction() + .is_some() + { + return Err(CompactionError::TransactionInProgress); + } + // Commit to free up any pending free pages + // Use 2-phase commit to avoid any possible security issues. Plus this compaction is going to be so slow that it doesn't matter. + // Once https://github.com/cberner/redb/issues/829 is fixed, we should upgrade this to use quick-repair -- that way the user + // can cancel the compaction without requiring a full repair afterwards + let mut txn = self.begin_write().map_err(|e| e.into_storage_error())?; + if txn.list_persistent_savepoints()?.next().is_some() { + return Err(CompactionError::PersistentSavepointExists); + } + if self.transaction_tracker.any_savepoint_exists() { + return Err(CompactionError::EphemeralSavepointExists); + } + txn.set_two_phase_commit(true); + txn.commit().map_err(|e| e.into_storage_error())?; + // Repeat, just in case executing list_persistent_savepoints() created a new table + let mut txn = self.begin_write().map_err(|e| e.into_storage_error())?; + txn.set_two_phase_commit(true); + txn.commit().map_err(|e| e.into_storage_error())?; + // There can't be any outstanding transactions because we have a `&mut self`, so all pending free pages + // should have been cleared out by the above commit() + let txn = self.begin_write().map_err(|e| e.into_storage_error())?; + assert!(!txn.pending_free_pages()?); + txn.abort()?; + + let mut compacted = false; + // Iteratively compact until no progress is made + loop { + let mut progress = false; + + let mut txn = self.begin_write().map_err(|e| e.into_storage_error())?; + if txn.compact_pages()? { + progress = true; + txn.commit().map_err(|e| e.into_storage_error())?; + } else { + txn.abort()?; + } + + // Double commit to free up the relocated pages for reuse + let mut txn = self.begin_write().map_err(|e| e.into_storage_error())?; + txn.set_two_phase_commit(true); + // Also shrink the database file by the maximum amount + txn.set_shrink_policy(ShrinkPolicy::Maximum); + txn.commit().map_err(|e| e.into_storage_error())?; + // Triple commit to free up the relocated pages for reuse + // TODO: this really shouldn't be necessary, but the data freed tree is a system table + // and so free'ing up its pages causes more deletes from the system tree + let mut txn = self.begin_write().map_err(|e| e.into_storage_error())?; + txn.set_two_phase_commit(true); + // Also shrink the database file by the maximum amount + txn.set_shrink_policy(ShrinkPolicy::Maximum); + txn.commit().map_err(|e| e.into_storage_error())?; + let txn = self.begin_write().map_err(|e| e.into_storage_error())?; + assert!(!txn.pending_free_pages()?); + txn.abort()?; + + if !progress { + break; + } + + compacted = true; + } + + Ok(compacted) + } + + #[cfg_attr(not(debug_assertions), expect(dead_code))] + fn check_repaired_allocated_pages_table( + system_root: Option, + mem: Arc, + ) -> Result { + let table_tree = TableTree::new( + system_root, + PageHint::None, + Arc::new(TransactionGuard::fake()), + mem.clone(), + )?; + if let Some(table_def) = table_tree + .get_table::( + DATA_ALLOCATED_TABLE.name(), + TableType::Normal, + ) + .map_err(|e| e.into_storage_error_or_corrupted("Allocated pages table corrupted"))? + { + let InternalTableDefinition::Normal { table_root, .. } = table_def else { + unreachable!() + }; + let table: ReadOnlyTable = ReadOnlyTable::new( + DATA_ALLOCATED_TABLE.name().to_string(), + table_root, + PageHint::None, + Arc::new(TransactionGuard::fake()), + mem.clone(), + )?; + for result in table.range::(..)? { + let (_, pages) = result?; + for i in 0..pages.value().len() { + assert!(mem.is_allocated(pages.value().get(i))); + } + } + } + + Ok(()) + } + + fn visit_freed_tree( + system_root: Option, + table_def: SystemTableDefinition, + mem: Arc, + mut visitor: F, + ) -> Result + where + F: FnMut(PageNumber) -> Result, + { + let fake_guard = Arc::new(TransactionGuard::fake()); + let system_tree = TableTree::new(system_root, PageHint::None, fake_guard, mem.clone())?; + let table_name = table_def.name(); + let result = match system_tree.get_table::(table_name, TableType::Normal) { + Ok(result) => result, + Err(TableError::Storage(err)) => { + return Err(err); + } + Err(TableError::TableDoesNotExist(_)) => { + return Ok(()); + } + Err(_) => { + return Err(StorageError::Corrupted(format!( + "Unable to open {table_name}" + ))); + } + }; + + if let Some(definition) = result { + let table_root = match definition { + InternalTableDefinition::Normal { table_root, .. } => table_root, + InternalTableDefinition::Multimap { .. } => unreachable!(), + }; + let table: ReadOnlyTable> = + ReadOnlyTable::new( + table_name.to_string(), + table_root, + PageHint::None, + Arc::new(TransactionGuard::fake()), + mem.clone(), + )?; + for result in table.range::(..)? { + let (_, page_list) = result?; + for i in 0..page_list.value().len() { + visitor(page_list.value().get(i))?; + } + } + } + + Ok(()) + } + + #[cfg(debug_assertions)] + fn mark_allocated_page_for_debug( + mem: &mut Arc, // Only &mut to ensure exclusivity + ) -> Result { + let data_root = mem.get_data_root(); + { + let fake = Arc::new(TransactionGuard::fake()); + let tables = TableTree::new(data_root, PageHint::None, fake, mem.clone())?; + tables.visit_all_pages(|path| { + mem.mark_debug_allocated_page(path.page_number()); + Ok(()) + })?; + } + + let system_root = mem.get_system_root(); + { + let fake = Arc::new(TransactionGuard::fake()); + let system_tables = TableTree::new(system_root, PageHint::None, fake, mem.clone())?; + system_tables.visit_all_pages(|path| { + mem.mark_debug_allocated_page(path.page_number()); + Ok(()) + })?; + } + + Self::visit_freed_tree(system_root, DATA_FREED_TABLE, mem.clone(), |page| { + mem.mark_debug_allocated_page(page); + Ok(()) + })?; + Self::visit_freed_tree(system_root, SYSTEM_FREED_TABLE, mem.clone(), |page| { + mem.mark_debug_allocated_page(page); + Ok(()) + })?; + + Ok(()) + } + + fn do_repair( + mem: &mut Arc, // Only &mut to ensure exclusivity + repair_callback: &(dyn Fn(&mut RepairSession) + 'static), + ) -> Result<[Option; 2], DatabaseError> { + if !Self::verify_primary_checksums(mem.clone())? { + if mem.used_two_phase_commit() { + return Err(DatabaseError::Storage(StorageError::Corrupted( + "Primary is corrupted despite 2-phase commit".to_string(), + ))); + } + + // 0.3 because the repair takes 3 full scans and the first is done now + let mut handle = RepairSession::new(0.3); + repair_callback(&mut handle); + if handle.aborted() { + return Err(DatabaseError::RepairAborted); + } + + mem.repair_primary_corrupted(); + // We need to invalidate the userspace cache, because walking the tree in verify_primary_checksums() may + // have poisoned it with pages that just got rolled back by repair_primary_corrupted(), since + // that rolls back a partially committed transaction. + mem.clear_read_cache(); + if !Self::verify_primary_checksums(mem.clone())? { + return Err(DatabaseError::Storage(StorageError::Corrupted( + "Failed to repair database. All roots are corrupted".to_string(), + ))); + } + } + // 0.6 because the repair takes 3 full scans and the second is done now + let mut handle = RepairSession::new(0.6); + repair_callback(&mut handle); + if handle.aborted() { + return Err(DatabaseError::RepairAborted); + } + + mem.begin_repair()?; + + let data_root = mem.get_data_root(); + { + let fake = Arc::new(TransactionGuard::fake()); + let tables = TableTree::new(data_root, PageHint::None, fake, mem.clone())?; + tables.visit_all_pages(|path| { + mem.mark_page_allocated(path.page_number()); + Ok(()) + })?; + } + + // 0.9 because the repair takes 3 full scans and the third is done now. There is just some system tables left + let mut handle = RepairSession::new(0.9); + repair_callback(&mut handle); + if handle.aborted() { + return Err(DatabaseError::RepairAborted); + } + + let system_root = mem.get_system_root(); + { + let fake = Arc::new(TransactionGuard::fake()); + let system_tables = TableTree::new(system_root, PageHint::None, fake, mem.clone())?; + system_tables.visit_all_pages(|path| { + mem.mark_page_allocated(path.page_number()); + Ok(()) + })?; + } + + Self::visit_freed_tree(system_root, DATA_FREED_TABLE, mem.clone(), |page| { + mem.mark_page_allocated(page); + Ok(()) + })?; + Self::visit_freed_tree(system_root, SYSTEM_FREED_TABLE, mem.clone(), |page| { + mem.mark_page_allocated(page); + Ok(()) + })?; + #[cfg(debug_assertions)] + { + Self::check_repaired_allocated_pages_table(system_root, mem.clone())?; + } + + mem.end_repair()?; + + // We need to invalidate the userspace cache, because we're about to implicitly free the freed table + // by storing an empty root during the below commit() + mem.clear_read_cache(); + + Ok([data_root, system_root]) + } + + fn new( + file: Box, + allow_initialize: bool, + page_size: usize, + region_size: Option, + cache_size: usize, + repair_callback: &(dyn Fn(&mut RepairSession) + 'static), + ) -> Result { + #[cfg(feature = "logging")] + let file_path = format!("{:?}", &file); + #[cfg(feature = "logging")] + info!("Opening database {:?}", &file_path); + let mem = TransactionalMemory::new( + file, + allow_initialize, + page_size, + region_size, + cache_size, + false, + )?; + let mut mem = Arc::new(mem); + // If the last transaction used 2-phase commit and updated the allocator state table, then + // we can just load the allocator state from there. Otherwise, we need a full repair + if let Some(tree) = Self::get_allocator_state_table(&mem)? { + #[cfg(feature = "logging")] + info!("Found valid allocator state, full repair not needed"); + mem.load_allocator_state(&tree)?; + #[cfg(debug_assertions)] + Self::mark_allocated_page_for_debug(&mut mem)?; + } else { + #[cfg(feature = "logging")] + warn!("Database {:?} not shutdown cleanly. Repairing", &file_path); + let mut handle = RepairSession::new(0.0); + repair_callback(&mut handle); + if handle.aborted() { + return Err(DatabaseError::RepairAborted); + } + let [data_root, system_root] = Self::do_repair(&mut mem, repair_callback)?; + let next_transaction_id = mem.get_last_committed_transaction_id()?.next(); + mem.commit( + data_root, + system_root, + next_transaction_id, + true, + ShrinkPolicy::Never, + )?; + } + + mem.begin_writable()?; + let next_transaction_id = mem.get_last_committed_transaction_id()?.next(); + + let db = Database { + mem, + transaction_tracker: Arc::new(TransactionTracker::new(next_transaction_id)), + }; + + // Restore the tracker state for any persistent savepoints + let txn = db.begin_write().map_err(|e| e.into_storage_error())?; + if let Some(next_id) = txn.next_persistent_savepoint_id()? { + db.transaction_tracker + .restore_savepoint_counter_state(next_id); + } + for id in txn.list_persistent_savepoints()? { + let savepoint = match txn.get_persistent_savepoint(id) { + Ok(savepoint) => savepoint, + Err(err) => match err { + SavepointError::InvalidSavepoint + | SavepointError::ImmediateDurabilityRequired => unreachable!(), + SavepointError::Storage(storage) => { + return Err(storage.into()); + } + }, + }; + db.transaction_tracker + .register_persistent_savepoint(&savepoint); + } + txn.abort()?; + + Ok(db) + } + + fn get_allocator_state_table( + mem: &Arc, + ) -> Result> { + // The allocator state table is only valid if the primary was written using 2-phase commit + if !mem.used_two_phase_commit() { + return Ok(None); + } + + // See if it's present in the system table tree + let system_table_tree = TableTree::new( + mem.get_system_root(), + PageHint::None, + Arc::new(TransactionGuard::fake()), + mem.clone(), + )?; + let Some(allocator_state_table) = system_table_tree + .get_table::(ALLOCATOR_STATE_TABLE_NAME, TableType::Normal) + .map_err(|e| e.into_storage_error_or_corrupted("Unexpected TableError"))? + else { + return Ok(None); + }; + + // Load the allocator state table + let InternalTableDefinition::Normal { table_root, .. } = allocator_state_table else { + unreachable!(); + }; + let tree = AllocatorStateTree::new( + table_root, + PageHint::None, + Arc::new(TransactionGuard::fake()), + mem.clone(), + )?; + + // Make sure this isn't stale allocator state left over from a previous transaction + if !mem.is_valid_allocator_state(&tree)? { + return Ok(None); + } + + Ok(Some(tree)) + } + + fn allocate_read_transaction(&self) -> Result { + let id = self + .transaction_tracker + .register_read_transaction(&self.mem)?; + + Ok(TransactionGuard::new_read( + id, + self.transaction_tracker.clone(), + )) + } + + /// Convenience method for [`Builder::new`] + pub fn builder() -> Builder { + Builder::new() + } + + /// Begins a write transaction + /// + /// Returns a [`WriteTransaction`] which may be used to read/write to the database. Only a single + /// write may be in progress at a time. If a write is in progress, this function will block + /// until it completes. + pub fn begin_write(&self) -> Result { + // Fail early if there has been an I/O error -- nothing can be committed in that case + self.mem.check_io_errors()?; + let guard = TransactionGuard::new_write( + self.transaction_tracker.start_write_transaction(), + self.transaction_tracker.clone(), + ); + WriteTransaction::new(guard, self.transaction_tracker.clone(), self.mem.clone()) + .map_err(|e| e.into()) + } + + fn ensure_allocator_state_table_and_trim(&self) -> Result<(), Error> { + // Make a new quick-repair commit to update the allocator state table + #[cfg(feature = "logging")] + debug!("Writing allocator state table"); + let mut tx = self.begin_write()?; + tx.set_quick_repair(true); + tx.set_shrink_policy(ShrinkPolicy::Maximum); + tx.commit()?; + + Ok(()) + } +} + +impl Drop for Database { + fn drop(&mut self) { + if !thread::panicking() && self.ensure_allocator_state_table_and_trim().is_err() { + #[cfg(feature = "logging")] + warn!("Failed to write allocator state table. Repair may be required at restart."); + } + + if self.mem.close().is_err() { + #[cfg(feature = "logging")] + warn!("Failed to flush database file. Repair may be required at restart."); + } + } +} + +pub struct RepairSession { + progress: f64, + aborted: bool, +} + +impl RepairSession { + pub(crate) fn new(progress: f64) -> Self { + Self { + progress, + aborted: false, + } + } + + pub(crate) fn aborted(&self) -> bool { + self.aborted + } + + /// Abort the repair process. The coorresponding call to [`Builder::open`] or [`Builder::create`] will return an error + pub fn abort(&mut self) { + self.aborted = true; + } + + /// Returns an estimate of the repair progress in the range [0.0, 1.0). At 1.0 the repair is complete. + pub fn progress(&self) -> f64 { + self.progress + } +} + +/// Configuration builder of a redb [Database]. +pub struct Builder { + page_size: usize, + region_size: Option, + cache_size: usize, + repair_callback: Box, +} + +impl Builder { + /// Construct a new [Builder] with sensible defaults. + /// + /// ## Defaults + /// + /// - `cache_size_bytes`: 1GiB + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { + // Default to 4k pages. Benchmarking showed that this was a good default on all platforms, + // including MacOS with 16k pages. Therefore, users are not allowed to configure it at the moment. + // It is part of the file format, so can be enabled in the future. + page_size: PAGE_SIZE, + region_size: None, + // TODO: Default should probably take into account the total system memory + cache_size: 1024 * 1024 * 1024, + repair_callback: Box::new(|_| {}), + } + } + + /// Set a callback which will be invoked periodically in the event that the database file needs + /// to be repaired. + /// + /// The [`RepairSession`] argument can be used to control the repair process. + /// + /// If the database file needs repair, the callback will be invoked at least once. + /// There is no upper limit on the number of times it may be called. + pub fn set_repair_callback( + &mut self, + callback: impl Fn(&mut RepairSession) + 'static, + ) -> &mut Self { + self.repair_callback = Box::new(callback); + self + } + + /// Set the internal page size of the database + /// + /// Valid values are powers of two, greater than or equal to 512 + /// + /// ## Defaults + /// + /// Default to 4 Kib pages. + #[cfg(any(fuzzing, test))] + pub fn set_page_size(&mut self, size: usize) -> &mut Self { + assert!(size.is_power_of_two()); + self.page_size = std::cmp::max(size, 512); + self + } + + /// Set the amount of memory (in bytes) used for caching data + pub fn set_cache_size(&mut self, bytes: usize) -> &mut Self { + self.cache_size = bytes; + self + } + + #[cfg(any(test, fuzzing))] + pub fn set_region_size(&mut self, size: u64) -> &mut Self { + assert!(size.is_power_of_two()); + self.region_size = Some(size); + self + } + + /// Opens the specified file as a redb database. + /// * if the file does not exist, or is an empty file, a new database will be initialized in it + /// * if the file is a valid redb database, it will be opened + /// * otherwise this function will return an error + pub fn create(&self, path: impl AsRef) -> Result { + let file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(path)?; + + Database::new( + Box::new(FileBackend::new(file)?), + true, + self.page_size, + self.region_size, + self.cache_size, + &self.repair_callback, + ) + } + + /// Opens an existing redb database. + pub fn open(&self, path: impl AsRef) -> Result { + let file = OpenOptions::new().read(true).write(true).open(path)?; + + Database::new( + Box::new(FileBackend::new(file)?), + false, + self.page_size, + None, + self.cache_size, + &self.repair_callback, + ) + } + + /// Opens an existing redb database. + /// + /// If the file has been opened for writing (i.e. as a [`Database`]) [`DatabaseError::DatabaseAlreadyOpen`] + /// will be returned on platforms which support file locks (macOS, Windows, Linux). On other platforms, + /// the caller MUST avoid calling this method when the database is open for writing. + pub fn open_read_only( + &self, + path: impl AsRef, + ) -> Result { + let file = OpenOptions::new().read(true).open(path)?; + + ReadOnlyDatabase::new( + Box::new(FileBackend::new_internal(file, true)?), + self.page_size, + None, + self.cache_size, + ) + } + + /// Open an existing or create a new database in the given `file`. + /// + /// The file must be empty or contain a valid database. + pub fn create_file(&self, file: File) -> Result { + Database::new( + Box::new(FileBackend::new(file)?), + true, + self.page_size, + self.region_size, + self.cache_size, + &self.repair_callback, + ) + } + + /// Open an existing or create a new database with the given backend. + pub fn create_with_backend( + &self, + backend: impl StorageBackend, + ) -> Result { + Database::new( + Box::new(backend), + true, + self.page_size, + self.region_size, + self.cache_size, + &self.repair_callback, + ) + } +} + +impl std::fmt::Debug for Database { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Database").finish() + } +} + +#[cfg(test)] +mod test { + use crate::backends::FileBackend; + use crate::{ + CommitError, Database, DatabaseError, Durability, ReadableTable, StorageBackend, + StorageError, TableDefinition, TransactionError, + }; + use std::fs::File; + use std::io::{ErrorKind, Read, Seek, SeekFrom}; + use std::sync::Arc; + use std::sync::atomic::{AtomicU64, Ordering}; + + #[derive(Debug)] + struct FailingBackend { + inner: FileBackend, + countdown: Arc, + } + + impl FailingBackend { + fn new(backend: FileBackend, countdown: u64) -> Self { + Self { + inner: backend, + countdown: Arc::new(AtomicU64::new(countdown)), + } + } + + fn check_countdown(&self) -> Result<(), std::io::Error> { + if self.countdown.load(Ordering::SeqCst) == 0 { + return Err(std::io::Error::from(ErrorKind::Other)); + } + + Ok(()) + } + + fn decrement_countdown(&self) -> Result<(), std::io::Error> { + if self + .countdown + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |x| { + if x > 0 { Some(x - 1) } else { None } + }) + .is_err() + { + return Err(std::io::Error::from(ErrorKind::Other)); + } + + Ok(()) + } + } + + impl StorageBackend for FailingBackend { + fn len(&self) -> Result { + self.inner.len() + } + + fn read(&self, offset: u64, out: &mut [u8]) -> Result<(), std::io::Error> { + self.check_countdown()?; + self.inner.read(offset, out) + } + + fn set_len(&self, len: u64) -> Result<(), std::io::Error> { + self.inner.set_len(len) + } + + fn sync_data(&self) -> Result<(), std::io::Error> { + self.check_countdown()?; + self.inner.sync_data() + } + + fn write(&self, offset: u64, data: &[u8]) -> Result<(), std::io::Error> { + self.decrement_countdown()?; + self.inner.write(offset, data) + } + } + + #[test] + fn crash_regression4() { + let tmpfile = crate::create_tempfile(); + let (file, path) = tmpfile.into_parts(); + + let backend = FailingBackend::new(FileBackend::new(file).unwrap(), 20); + let db = Database::builder() + .set_cache_size(12686) + .set_page_size(8 * 1024) + .set_region_size(32 * 4096) + .create_with_backend(backend) + .unwrap(); + + let table_def: TableDefinition = TableDefinition::new("x"); + + let tx = db.begin_write().unwrap(); + let _savepoint = tx.ephemeral_savepoint().unwrap(); + let _persistent_savepoint = tx.persistent_savepoint().unwrap(); + tx.commit().unwrap(); + let tx = db.begin_write().unwrap(); + { + let mut table = tx.open_table(table_def).unwrap(); + let _ = table.insert_reserve(118821, 360).unwrap(); + } + let result = tx.commit(); + assert!(result.is_err()); + + drop(db); + Database::builder() + .set_cache_size(1024 * 1024) + .set_page_size(8 * 1024) + .set_region_size(32 * 4096) + .create(&path) + .unwrap(); + } + + #[test] + fn transient_io_error() { + let tmpfile = crate::create_tempfile(); + let (file, path) = tmpfile.into_parts(); + + let backend = FailingBackend::new(FileBackend::new(file).unwrap(), u64::MAX); + let countdown = backend.countdown.clone(); + let db = Database::builder() + .set_cache_size(0) + .create_with_backend(backend) + .unwrap(); + + let table_def: TableDefinition = TableDefinition::new("x"); + + // Create some garbage + let tx = db.begin_write().unwrap(); + { + let mut table = tx.open_table(table_def).unwrap(); + table.insert(0, 0).unwrap(); + } + tx.commit().unwrap(); + let tx = db.begin_write().unwrap(); + { + let mut table = tx.open_table(table_def).unwrap(); + table.insert(0, 1).unwrap(); + } + tx.commit().unwrap(); + + let tx = db.begin_write().unwrap(); + // Cause an error in the commit + countdown.store(0, Ordering::SeqCst); + let result = tx.commit().err().unwrap(); + assert!(matches!(result, CommitError::Storage(StorageError::Io(_)))); + let result = db.begin_write().err().unwrap(); + assert!(matches!( + result, + TransactionError::Storage(StorageError::PreviousIo) + )); + // Simulate a transient error + countdown.store(u64::MAX, Ordering::SeqCst); + drop(db); + + // Check that recovery flag is set, even though the error has "cleared" + let mut file = File::open(&path).unwrap(); + file.seek(SeekFrom::Start(9)).unwrap(); + let mut god_byte = vec![0u8]; + assert_eq!(file.read(&mut god_byte).unwrap(), 1); + assert_ne!(god_byte[0] & 2, 0); + } + + #[test] + fn small_pages() { + let tmpfile = crate::create_tempfile(); + + let db = Database::builder() + .set_page_size(512) + .create(tmpfile.path()) + .unwrap(); + + let table_definition: TableDefinition = TableDefinition::new("x"); + let txn = db.begin_write().unwrap(); + { + txn.open_table(table_definition).unwrap(); + } + txn.commit().unwrap(); + } + + #[test] + fn small_pages2() { + let tmpfile = crate::create_tempfile(); + + let db = Database::builder() + .set_page_size(512) + .create(tmpfile.path()) + .unwrap(); + + let table_def: TableDefinition = TableDefinition::new("x"); + + let mut tx = db.begin_write().unwrap(); + tx.set_two_phase_commit(true); + let savepoint0 = tx.ephemeral_savepoint().unwrap(); + { + tx.open_table(table_def).unwrap(); + } + tx.commit().unwrap(); + + let mut tx = db.begin_write().unwrap(); + tx.set_two_phase_commit(true); + let savepoint1 = tx.ephemeral_savepoint().unwrap(); + tx.restore_savepoint(&savepoint0).unwrap(); + tx.set_durability(Durability::None).unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + t.insert_reserve(&660503, 489).unwrap().as_mut().fill(0xFF); + assert!(t.remove(&291295).unwrap().is_none()); + } + tx.commit().unwrap(); + + let mut tx = db.begin_write().unwrap(); + tx.set_two_phase_commit(true); + tx.restore_savepoint(&savepoint0).unwrap(); + { + tx.open_table(table_def).unwrap(); + } + tx.commit().unwrap(); + + let mut tx = db.begin_write().unwrap(); + tx.set_two_phase_commit(true); + let savepoint2 = tx.ephemeral_savepoint().unwrap(); + drop(savepoint0); + tx.restore_savepoint(&savepoint2).unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + assert!(t.get(&2059).unwrap().is_none()); + assert!(t.remove(&145227).unwrap().is_none()); + assert!(t.remove(&145227).unwrap().is_none()); + } + tx.commit().unwrap(); + + let mut tx = db.begin_write().unwrap(); + tx.set_two_phase_commit(true); + let savepoint3 = tx.ephemeral_savepoint().unwrap(); + drop(savepoint1); + tx.restore_savepoint(&savepoint3).unwrap(); + { + tx.open_table(table_def).unwrap(); + } + tx.commit().unwrap(); + + let mut tx = db.begin_write().unwrap(); + tx.set_two_phase_commit(true); + let savepoint4 = tx.ephemeral_savepoint().unwrap(); + drop(savepoint2); + tx.restore_savepoint(&savepoint3).unwrap(); + tx.set_durability(Durability::None).unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + assert!(t.remove(&207936).unwrap().is_none()); + } + tx.abort().unwrap(); + + let mut tx = db.begin_write().unwrap(); + tx.set_two_phase_commit(true); + let _savepoint5 = tx.ephemeral_savepoint().unwrap(); + drop(savepoint3); + // savepoint4 was invalidated by the restore_savepoint(savepoint3) call + // above, but that transaction was aborted, so the invalidation is + // reversed and savepoint4 is valid again. Restoring it here invalidates + // savepoint5 (which is newer), so the next transaction restores + // savepoint4 again rather than savepoint5. + tx.restore_savepoint(&savepoint4).unwrap(); + { + tx.open_table(table_def).unwrap(); + } + tx.commit().unwrap(); + + let mut tx = db.begin_write().unwrap(); + tx.set_two_phase_commit(true); + tx.restore_savepoint(&savepoint4).unwrap(); + tx.set_durability(Durability::None).unwrap(); + { + tx.open_table(table_def).unwrap(); + } + tx.commit().unwrap(); + } + + #[test] + fn small_pages3() { + let tmpfile = crate::create_tempfile(); + + let db = Database::builder() + .set_page_size(1024) + .create(tmpfile.path()) + .unwrap(); + + let table_def: TableDefinition = TableDefinition::new("x"); + + let mut tx = db.begin_write().unwrap(); + let _savepoint0 = tx.ephemeral_savepoint().unwrap(); + tx.set_durability(Durability::None).unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + let value = vec![0; 306]; + t.insert(&539717, value.as_slice()).unwrap(); + } + tx.abort().unwrap(); + + let mut tx = db.begin_write().unwrap(); + let savepoint1 = tx.ephemeral_savepoint().unwrap(); + tx.restore_savepoint(&savepoint1).unwrap(); + tx.set_durability(Durability::None).unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + let value = vec![0; 2008]; + t.insert(&784384, value.as_slice()).unwrap(); + } + tx.abort().unwrap(); + } + + #[test] + fn small_pages4() { + let tmpfile = crate::create_tempfile(); + + let db = Database::builder() + .set_cache_size(1024 * 1024) + .set_page_size(1024) + .create(tmpfile.path()) + .unwrap(); + + let table_def: TableDefinition = TableDefinition::new("x"); + + let tx = db.begin_write().unwrap(); + { + tx.open_table(table_def).unwrap(); + } + tx.commit().unwrap(); + + let tx = db.begin_write().unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + assert!(t.get(&131072).unwrap().is_none()); + let value = vec![0xFF; 1130]; + t.insert(&42394, value.as_slice()).unwrap(); + t.insert_reserve(&744037, 3645).unwrap().as_mut().fill(0xFF); + assert!(t.get(&0).unwrap().is_none()); + } + tx.abort().unwrap(); + + let tx = db.begin_write().unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + t.insert_reserve(&118749, 734).unwrap().as_mut().fill(0xFF); + } + tx.abort().unwrap(); + } + + #[test] + fn dynamic_shrink() { + let tmpfile = crate::create_tempfile(); + let table_definition: TableDefinition = TableDefinition::new("x"); + let big_value = vec![0u8; 1024]; + + let db = Database::builder() + .set_region_size(1024 * 1024) + .create(tmpfile.path()) + .unwrap(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(table_definition).unwrap(); + for i in 0..2048 { + table.insert(&i, big_value.as_slice()).unwrap(); + } + } + txn.commit().unwrap(); + + let file_size = tmpfile.as_file().metadata().unwrap().len(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(table_definition).unwrap(); + for i in 0..2048 { + table.remove(&i).unwrap(); + } + } + txn.commit().unwrap(); + + // Perform a couple more commits to be sure the database has a chance to compact + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(table_definition).unwrap(); + table.insert(0, [].as_slice()).unwrap(); + } + txn.commit().unwrap(); + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(table_definition).unwrap(); + table.remove(0).unwrap(); + } + txn.commit().unwrap(); + let txn = db.begin_write().unwrap(); + txn.commit().unwrap(); + + let final_file_size = tmpfile.as_file().metadata().unwrap().len(); + assert!(final_file_size < file_size); + } + + #[test] + fn create_new_db_in_empty_file() { + let tmpfile = crate::create_tempfile(); + + let _db = Database::builder() + .create_file(tmpfile.into_file()) + .unwrap(); + } + + #[test] + fn open_missing_file() { + let tmpfile = crate::create_tempfile(); + + let err = Database::builder() + .open(tmpfile.path().with_extension("missing")) + .unwrap_err(); + + match err { + DatabaseError::Storage(StorageError::Io(err)) if err.kind() == ErrorKind::NotFound => {} + err => panic!("Unexpected error for empty file: {err}"), + } + } + + #[test] + fn open_empty_file() { + let tmpfile = crate::create_tempfile(); + + let err = Database::builder().open(tmpfile.path()).unwrap_err(); + + match err { + DatabaseError::Storage(StorageError::Io(err)) + if err.kind() == ErrorKind::InvalidData => {} + err => panic!("Unexpected error for empty file: {err}"), + } + } +} diff --git a/v2/redb/src/error.rs b/v2/redb/src/error.rs new file mode 100644 index 000000000..1a6b2da60 --- /dev/null +++ b/v2/redb/src/error.rs @@ -0,0 +1,693 @@ +use crate::tree_store::{FILE_FORMAT_VERSION3, MAX_VALUE_LENGTH}; +use crate::{ReadTransaction, TypeName}; +use std::fmt::{Display, Formatter}; +use std::sync::PoisonError; +use std::{io, panic}; + +/// General errors directly from the storage layer +#[derive(Debug)] +#[non_exhaustive] +pub enum StorageError { + /// The Database is corrupted + Corrupted(String), + /// The value being inserted exceeds the maximum of 3GiB + ValueTooLarge(usize), + Io(io::Error), + PreviousIo, + DatabaseClosed, + LockPoisoned(&'static panic::Location<'static>), +} + +impl From> for StorageError { + fn from(_: PoisonError) -> StorageError { + StorageError::LockPoisoned(panic::Location::caller()) + } +} + +impl From for StorageError { + fn from(err: io::Error) -> StorageError { + StorageError::Io(err) + } +} + +impl From for Error { + fn from(err: StorageError) -> Error { + match err { + StorageError::Corrupted(msg) => Error::Corrupted(msg), + StorageError::ValueTooLarge(x) => Error::ValueTooLarge(x), + StorageError::Io(x) => Error::Io(x), + StorageError::PreviousIo => Error::PreviousIo, + StorageError::DatabaseClosed => Error::DatabaseClosed, + StorageError::LockPoisoned(location) => Error::LockPoisoned(location), + } + } +} + +impl Display for StorageError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + StorageError::Corrupted(msg) => { + write!(f, "DB corrupted: {msg}") + } + StorageError::ValueTooLarge(len) => { + write!( + f, + "The value (length={len}) being inserted exceeds the maximum of {}GiB", + MAX_VALUE_LENGTH / 1024 / 1024 / 1024 + ) + } + StorageError::Io(err) => { + write!(f, "I/O error: {err}") + } + StorageError::DatabaseClosed => { + write!(f, "Database has been closed") + } + StorageError::PreviousIo => { + write!( + f, + "Previous I/O error occurred. Please close and re-open the database." + ) + } + StorageError::LockPoisoned(location) => { + write!(f, "Poisoned internal lock: {location}") + } + } + } +} + +impl std::error::Error for StorageError {} + +/// Errors related to opening tables +#[derive(Debug)] +#[non_exhaustive] +pub enum TableError { + /// Table types didn't match. + TableTypeMismatch { + table: String, + key: TypeName, + value: TypeName, + }, + /// The table is a multimap table + TableIsMultimap(String), + /// The table is not a multimap table + TableIsNotMultimap(String), + TypeDefinitionChanged { + name: TypeName, + alignment: usize, + width: Option, + }, + /// Table name does not match any table in database + TableDoesNotExist(String), + /// Table name already exists in the database + TableExists(String), + // Tables cannot be opened for writing multiple times, since they could retrieve immutable & + // mutable references to the same dirty pages, or multiple mutable references via insert_reserve() + TableAlreadyOpen(String, &'static panic::Location<'static>), + /// Error from underlying storage + Storage(StorageError), +} + +impl TableError { + pub(crate) fn into_storage_error_or_corrupted(self, msg: &str) -> StorageError { + match self { + TableError::TableTypeMismatch { .. } + | TableError::TableIsMultimap(_) + | TableError::TableIsNotMultimap(_) + | TableError::TypeDefinitionChanged { .. } + | TableError::TableDoesNotExist(_) + | TableError::TableExists(_) + | TableError::TableAlreadyOpen(_, _) => { + StorageError::Corrupted(format!("{msg}: {self}")) + } + TableError::Storage(storage) => storage, + } + } +} + +impl From for Error { + fn from(err: TableError) -> Error { + match err { + TableError::TypeDefinitionChanged { + name, + alignment, + width, + } => Error::TypeDefinitionChanged { + name, + alignment, + width, + }, + TableError::TableTypeMismatch { table, key, value } => { + Error::TableTypeMismatch { table, key, value } + } + TableError::TableIsMultimap(table) => Error::TableIsMultimap(table), + TableError::TableIsNotMultimap(table) => Error::TableIsNotMultimap(table), + TableError::TableDoesNotExist(table) => Error::TableDoesNotExist(table), + TableError::TableExists(table) => Error::TableExists(table), + TableError::TableAlreadyOpen(name, location) => Error::TableAlreadyOpen(name, location), + TableError::Storage(storage) => storage.into(), + } + } +} + +impl From for TableError { + fn from(err: StorageError) -> TableError { + TableError::Storage(err) + } +} + +impl Display for TableError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + TableError::TypeDefinitionChanged { + name, + alignment, + width, + } => { + write!( + f, + "Current definition of {} does not match stored definition (width={:?}, alignment={})", + name.name(), + width, + alignment, + ) + } + TableError::TableTypeMismatch { table, key, value } => { + write!( + f, + "{table} is of type Table<{}, {}>", + key.name(), + value.name(), + ) + } + TableError::TableIsMultimap(table) => { + write!(f, "{table} is a multimap table") + } + TableError::TableIsNotMultimap(table) => { + write!(f, "{table} is not a multimap table") + } + TableError::TableDoesNotExist(table) => { + write!(f, "Table '{table}' does not exist") + } + TableError::TableExists(table) => { + write!(f, "Table '{table}' already exists") + } + TableError::TableAlreadyOpen(name, location) => { + write!(f, "Table '{name}' already opened at: {location}") + } + TableError::Storage(storage) => storage.fmt(f), + } + } +} + +impl std::error::Error for TableError {} + +/// Errors related to opening a database +#[derive(Debug)] +#[non_exhaustive] +pub enum DatabaseError { + /// The Database is already open. Cannot acquire lock. + DatabaseAlreadyOpen, + /// [`crate::RepairSession::abort`] was called or repair was aborted for another reason (such as the database being read-only). + RepairAborted, + /// The database file is in an old file format and must be manually upgraded + UpgradeRequired(u8), + /// A transaction is still in-progress + TransactionInProgress, + /// Error from underlying storage + Storage(StorageError), +} + +impl From for Error { + fn from(err: DatabaseError) -> Error { + match err { + DatabaseError::DatabaseAlreadyOpen => Error::DatabaseAlreadyOpen, + DatabaseError::RepairAborted => Error::RepairAborted, + DatabaseError::UpgradeRequired(x) => Error::UpgradeRequired(x), + DatabaseError::TransactionInProgress => Error::TransactionInProgress, + DatabaseError::Storage(storage) => storage.into(), + } + } +} + +impl From for DatabaseError { + fn from(err: io::Error) -> DatabaseError { + DatabaseError::Storage(StorageError::Io(err)) + } +} + +impl From for DatabaseError { + fn from(err: StorageError) -> DatabaseError { + DatabaseError::Storage(err) + } +} + +impl Display for DatabaseError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + DatabaseError::UpgradeRequired(actual) => { + write!( + f, + "Manual upgrade required. Expected file format version {FILE_FORMAT_VERSION3}, but file is version {actual}" + ) + } + DatabaseError::RepairAborted => { + write!(f, "Database repair aborted.") + } + DatabaseError::DatabaseAlreadyOpen => { + write!(f, "Database already open. Cannot acquire lock.") + } + DatabaseError::TransactionInProgress => { + write!( + f, + "A transaction is still in progress. Operation cannot be performed." + ) + } + DatabaseError::Storage(storage) => storage.fmt(f), + } + } +} + +impl std::error::Error for DatabaseError {} + +/// Errors related to savepoints +#[derive(Debug)] +#[non_exhaustive] +pub enum SavepointError { + /// This savepoint is invalid or cannot be created. + /// + /// Savepoints become invalid when an older savepoint is restored after it was created, + /// and savepoints cannot be created if the transaction is "dirty" (any tables have been opened) + InvalidSavepoint, + /// The operation requires the transaction's durability to be [`crate::Durability::Immediate`]. + /// + /// Returned by operations that must be able to modify persistent savepoints, such as + /// creating or deleting a persistent savepoint, or restoring an older savepoint while + /// newer persistent savepoints exist that would need to be deleted. + ImmediateDurabilityRequired, + /// Error from underlying storage + Storage(StorageError), +} + +impl From for Error { + fn from(err: SavepointError) -> Error { + match err { + SavepointError::InvalidSavepoint => Error::InvalidSavepoint, + SavepointError::ImmediateDurabilityRequired => Error::ImmediateDurabilityRequired, + SavepointError::Storage(storage) => storage.into(), + } + } +} + +impl From for SavepointError { + fn from(err: StorageError) -> SavepointError { + SavepointError::Storage(err) + } +} + +impl Display for SavepointError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + SavepointError::InvalidSavepoint => { + write!(f, "Savepoint is invalid or cannot be created.") + } + SavepointError::ImmediateDurabilityRequired => { + write!( + f, + "Operation requires Durability::Immediate for the current transaction." + ) + } + SavepointError::Storage(storage) => storage.fmt(f), + } + } +} + +impl std::error::Error for SavepointError {} + +/// Errors related to compaction +#[derive(Debug)] +#[non_exhaustive] +pub enum CompactionError { + /// A persistent savepoint exists + PersistentSavepointExists, + /// A ephemeral savepoint exists + EphemeralSavepointExists, + /// A transaction is still in-progress + TransactionInProgress, + /// Error from underlying storage + Storage(StorageError), +} + +impl From for Error { + fn from(err: CompactionError) -> Error { + match err { + CompactionError::PersistentSavepointExists => Error::PersistentSavepointExists, + CompactionError::EphemeralSavepointExists => Error::EphemeralSavepointExists, + CompactionError::TransactionInProgress => Error::TransactionInProgress, + CompactionError::Storage(storage) => storage.into(), + } + } +} + +impl From for CompactionError { + fn from(err: StorageError) -> CompactionError { + CompactionError::Storage(err) + } +} + +impl Display for CompactionError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + CompactionError::PersistentSavepointExists => { + write!( + f, + "Persistent savepoint exists. Operation cannot be performed." + ) + } + CompactionError::EphemeralSavepointExists => { + write!( + f, + "Ephemeral savepoint exists. Operation cannot be performed." + ) + } + CompactionError::TransactionInProgress => { + write!( + f, + "A transaction is still in progress. Operation cannot be performed." + ) + } + CompactionError::Storage(storage) => storage.fmt(f), + } + } +} + +impl std::error::Error for CompactionError {} + +/// Errors related to transactions +#[derive(Debug)] +#[non_exhaustive] +pub enum SetDurabilityError { + /// A persistent savepoint was modified + PersistentSavepointModified, +} + +impl From for Error { + fn from(err: SetDurabilityError) -> Error { + match err { + SetDurabilityError::PersistentSavepointModified => Error::PersistentSavepointModified, + } + } +} + +impl Display for SetDurabilityError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + SetDurabilityError::PersistentSavepointModified => { + write!( + f, + "Persistent savepoint modified. Cannot reduce transaction durability" + ) + } + } + } +} + +impl std::error::Error for SetDurabilityError {} + +/// Errors related to transactions +#[derive(Debug)] +#[non_exhaustive] +pub enum TransactionError { + /// Error from underlying storage + Storage(StorageError), + /// The transaction is still referenced by a table or other object + ReadTransactionStillInUse(Box), +} + +impl TransactionError { + pub(crate) fn into_storage_error(self) -> StorageError { + match self { + TransactionError::Storage(storage) => storage, + _ => unreachable!(), + } + } +} + +impl From for Error { + fn from(err: TransactionError) -> Error { + match err { + TransactionError::Storage(storage) => storage.into(), + TransactionError::ReadTransactionStillInUse(txn) => { + Error::ReadTransactionStillInUse(txn) + } + } + } +} + +impl From for TransactionError { + fn from(err: StorageError) -> TransactionError { + TransactionError::Storage(err) + } +} + +impl Display for TransactionError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + TransactionError::Storage(storage) => storage.fmt(f), + TransactionError::ReadTransactionStillInUse(_) => { + write!(f, "Transaction still in use") + } + } + } +} + +impl std::error::Error for TransactionError {} + +/// Errors related to committing transactions +#[derive(Debug)] +#[non_exhaustive] +pub enum CommitError { + /// Error from underlying storage + Storage(StorageError), +} + +impl CommitError { + pub(crate) fn into_storage_error(self) -> StorageError { + match self { + CommitError::Storage(storage) => storage, + } + } +} + +impl From for Error { + fn from(err: CommitError) -> Error { + match err { + CommitError::Storage(storage) => storage.into(), + } + } +} + +impl From for CommitError { + fn from(err: StorageError) -> CommitError { + CommitError::Storage(err) + } +} + +impl Display for CommitError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + CommitError::Storage(storage) => storage.fmt(f), + } + } +} + +impl std::error::Error for CommitError {} + +/// Superset of all other errors that can occur. Convenience enum so that users can convert all errors into a single type +#[derive(Debug)] +#[non_exhaustive] +pub enum Error { + /// The Database is already open. Cannot acquire lock. + DatabaseAlreadyOpen, + /// This savepoint is invalid or cannot be created. + /// + /// Savepoints become invalid when an older savepoint is restored after it was created, + /// and savepoints cannot be created if the transaction is "dirty" (any tables have been opened) + InvalidSavepoint, + /// A savepoint operation requires [`crate::Durability::Immediate`] for the transaction. + ImmediateDurabilityRequired, + /// [`crate::RepairSession::abort`] was called. + RepairAborted, + /// A persistent savepoint was modified + PersistentSavepointModified, + /// A persistent savepoint exists + PersistentSavepointExists, + /// An Ephemeral savepoint exists + EphemeralSavepointExists, + /// A transaction is still in-progress + TransactionInProgress, + /// The Database is corrupted + Corrupted(String), + /// The database file is in an old file format and must be manually upgraded + UpgradeRequired(u8), + /// The value being inserted exceeds the maximum of 3GiB + ValueTooLarge(usize), + /// Table types didn't match. + TableTypeMismatch { + table: String, + key: TypeName, + value: TypeName, + }, + /// The table is a multimap table + TableIsMultimap(String), + /// The table is not a multimap table + TableIsNotMultimap(String), + TypeDefinitionChanged { + name: TypeName, + alignment: usize, + width: Option, + }, + /// Table name does not match any table in database + TableDoesNotExist(String), + /// Table name already exists in the database + TableExists(String), + // Tables cannot be opened for writing multiple times, since they could retrieve immutable & + // mutable references to the same dirty pages, or multiple mutable references via insert_reserve() + TableAlreadyOpen(String, &'static panic::Location<'static>), + Io(io::Error), + DatabaseClosed, + /// A previous IO error occurred. The database must be closed and re-opened + PreviousIo, + LockPoisoned(&'static panic::Location<'static>), + /// The transaction is still referenced by a table or other object + ReadTransactionStillInUse(Box), +} + +impl From> for Error { + fn from(_: PoisonError) -> Error { + Error::LockPoisoned(panic::Location::caller()) + } +} + +impl From for Error { + fn from(err: io::Error) -> Error { + Error::Io(err) + } +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Error::Corrupted(msg) => { + write!(f, "DB corrupted: {msg}") + } + Error::UpgradeRequired(actual) => { + write!( + f, + "Manual upgrade required. Expected file format version {FILE_FORMAT_VERSION3}, but file is version {actual}" + ) + } + Error::ValueTooLarge(len) => { + write!( + f, + "The value (length={len}) being inserted exceeds the maximum of {}GiB", + MAX_VALUE_LENGTH / 1024 / 1024 / 1024 + ) + } + Error::TypeDefinitionChanged { + name, + alignment, + width, + } => { + write!( + f, + "Current definition of {} does not match stored definition (width={:?}, alignment={})", + name.name(), + width, + alignment, + ) + } + Error::TableTypeMismatch { table, key, value } => { + write!( + f, + "{table} is of type Table<{}, {}>", + key.name(), + value.name(), + ) + } + Error::TableIsMultimap(table) => { + write!(f, "{table} is a multimap table") + } + Error::TableIsNotMultimap(table) => { + write!(f, "{table} is not a multimap table") + } + Error::TableDoesNotExist(table) => { + write!(f, "Table '{table}' does not exist") + } + Error::TableExists(table) => { + write!(f, "Table '{table}' already exists") + } + Error::TableAlreadyOpen(name, location) => { + write!(f, "Table '{name}' already opened at: {location}") + } + Error::Io(err) => { + write!(f, "I/O error: {err}") + } + Error::DatabaseClosed => { + write!(f, "Database has been closed") + } + Error::PreviousIo => { + write!( + f, + "Previous I/O error occurred. Please close and re-open the database." + ) + } + Error::LockPoisoned(location) => { + write!(f, "Poisoned internal lock: {location}") + } + Error::DatabaseAlreadyOpen => { + write!(f, "Database already open. Cannot acquire lock.") + } + Error::RepairAborted => { + write!(f, "Database repair aborted.") + } + Error::PersistentSavepointModified => { + write!( + f, + "Persistent savepoint modified. Cannot reduce transaction durability" + ) + } + Error::PersistentSavepointExists => { + write!( + f, + "Persistent savepoint exists. Operation cannot be performed." + ) + } + Error::EphemeralSavepointExists => { + write!( + f, + "Ephemeral savepoint exists. Operation cannot be performed." + ) + } + Error::TransactionInProgress => { + write!( + f, + "A transaction is still in progress. Operation cannot be performed." + ) + } + Error::InvalidSavepoint => { + write!(f, "Savepoint is invalid or cannot be created.") + } + Error::ImmediateDurabilityRequired => { + write!( + f, + "Operation requires Durability::Immediate for the current transaction." + ) + } + Error::ReadTransactionStillInUse(_) => { + write!(f, "Transaction still in use") + } + } + } +} + +impl std::error::Error for Error {} diff --git a/v2/redb/src/lib.rs b/v2/redb/src/lib.rs new file mode 100644 index 000000000..4923b921e --- /dev/null +++ b/v2/redb/src/lib.rs @@ -0,0 +1,108 @@ +#![deny(clippy::all, clippy::pedantic, clippy::disallowed_methods)] +// TODO: revisit this list and see if we can enable some +#![allow( + clippy::default_trait_access, + clippy::if_not_else, + clippy::iter_not_returning_iterator, + clippy::missing_errors_doc, + clippy::missing_panics_doc, + clippy::module_name_repetitions, + clippy::must_use_candidate, + clippy::needless_pass_by_value, + clippy::redundant_closure_for_method_calls, + clippy::similar_names, + clippy::too_many_lines, + clippy::unnecessary_wraps, + clippy::unreadable_literal +)] + +//! # redb +//! +//! A simple, portable, high-performance, ACID, embedded key-value store. +//! +//! redb is written in pure Rust and is loosely inspired by [lmdb][lmdb]. Data is stored in a collection +//! of copy-on-write B-trees. For more details, see the [design doc][design]. +//! +//! # Features +//! +//! - Zero-copy, thread-safe, `BTreeMap` based API +//! - Fully ACID-compliant transactions +//! - MVCC support for concurrent readers & writer, without blocking +//! - Crash-safe by default +//! - Savepoints and rollbacks +//! +//! # Example +//! +//! ``` +//! use redb::{Database, Error, ReadableDatabase, ReadableTable, TableDefinition}; +//! +//! const TABLE: TableDefinition<&str, u64> = TableDefinition::new("my_data"); +//! +//! fn main() -> Result<(), Error> { +//! # #[cfg(not(target_os = "wasi"))] +//! let file = tempfile::NamedTempFile::new().unwrap(); +//! # #[cfg(target_os = "wasi")] +//! # let file = tempfile::NamedTempFile::new_in("/tmp").unwrap(); +//! let db = Database::create(file.path())?; +//! let write_txn = db.begin_write()?; +//! { +//! let mut table = write_txn.open_table(TABLE)?; +//! table.insert("my_key", &123)?; +//! } +//! write_txn.commit()?; +//! +//! let read_txn = db.begin_read()?; +//! let table = read_txn.open_table(TABLE)?; +//! assert_eq!(table.get("my_key")?.unwrap().value(), 123); +//! +//! Ok(()) +//! } +//! ``` +//! +//! [lmdb]: https://www.lmdb.tech/doc/ +//! [design]: https://github.com/cberner/redb/blob/master/docs/design.md + +pub use db::{ + Builder, CacheStats, Database, MultimapTableDefinition, MultimapTableHandle, ReadOnlyDatabase, + ReadableDatabase, RepairSession, StorageBackend, TableDefinition, TableHandle, + UntypedMultimapTableHandle, UntypedTableHandle, +}; +pub use error::{ + CommitError, CompactionError, DatabaseError, Error, SavepointError, SetDurabilityError, + StorageError, TableError, TransactionError, +}; +pub use multimap_table::{ + MultimapRange, MultimapTable, MultimapValue, ReadOnlyMultimapTable, + ReadOnlyUntypedMultimapTable, ReadableMultimapTable, +}; +pub use table::{ + ExtractIf, Range, ReadOnlyTable, ReadOnlyUntypedTable, ReadableTable, ReadableTableMetadata, + Table, TableStats, +}; +pub use transactions::{DatabaseStats, Durability, ReadTransaction, WriteTransaction}; +pub use tree_store::{AccessGuard, AccessGuardMut, AccessGuardMutInPlace, Savepoint}; +pub use types::{Key, MutInPlaceValue, TypeName, Value}; + +pub type Result = std::result::Result; + +pub mod backends; +mod complex_types; +mod db; +mod error; +mod multimap_table; +mod sealed; +mod table; +mod transaction_tracker; +mod transactions; +mod tree_store; +mod tuple_types; +mod types; + +#[cfg(test)] +fn create_tempfile() -> tempfile::NamedTempFile { + if cfg!(target_os = "wasi") { + tempfile::NamedTempFile::new_in("/tmp").unwrap() + } else { + tempfile::NamedTempFile::new().unwrap() + } +} diff --git a/v2/redb/src/multimap_table.rs b/v2/redb/src/multimap_table.rs new file mode 100644 index 000000000..275f8a263 --- /dev/null +++ b/v2/redb/src/multimap_table.rs @@ -0,0 +1,1653 @@ +use crate::db::TransactionGuard; +use crate::multimap_table::DynamicCollectionType::{Inline, SubtreeV2}; +use crate::sealed::Sealed; +use crate::table::{ReadableTableMetadata, TableStats}; +use crate::tree_store::{ + AllPageNumbersBtreeIter, BRANCH, BranchAccessor, BranchMutator, Btree, BtreeHeader, BtreeMut, + BtreeRangeIter, BtreeStats, Checksum, DEFERRED, LEAF, LeafAccessor, LeafMutator, + MAX_PAIR_LENGTH, MAX_VALUE_LENGTH, Page, PageHint, PageNumber, PagePath, PageTrackerPolicy, + RawBtree, RawLeafBuilder, TransactionalMemory, UntypedBtree, UntypedBtreeMut, btree_stats, +}; +use crate::types::{Key, TypeName, Value}; +use crate::{AccessGuard, MultimapTableHandle, Result, StorageError, WriteTransaction}; +use std::borrow::Borrow; +use std::cmp::max; +use std::collections::HashMap; +use std::convert::TryInto; +use std::marker::PhantomData; +use std::mem; +use std::mem::size_of; +use std::ops::{RangeBounds, RangeFull}; +use std::sync::{Arc, Mutex}; + +pub(crate) fn multimap_btree_stats( + root: Option, + mem: &TransactionalMemory, + fixed_key_size: Option, + fixed_value_size: Option, + hint: PageHint, +) -> Result { + if let Some(root) = root { + multimap_stats_helper(root, mem, fixed_key_size, fixed_value_size, hint) + } else { + Ok(BtreeStats { + tree_height: 0, + leaf_pages: 0, + branch_pages: 0, + stored_leaf_bytes: 0, + metadata_bytes: 0, + fragmented_bytes: 0, + }) + } +} + +fn multimap_stats_helper( + page_number: PageNumber, + mem: &TransactionalMemory, + fixed_key_size: Option, + fixed_value_size: Option, + hint: PageHint, +) -> Result { + let page = mem.get_page(page_number, hint)?; + let node_mem = page.memory(); + match node_mem[0] { + LEAF => { + let accessor = LeafAccessor::new( + page.memory(), + fixed_key_size, + DynamicCollection::<()>::fixed_width_with(fixed_value_size), + ); + let mut leaf_bytes = 0u64; + let mut is_branch = false; + for i in 0..accessor.num_pairs() { + let entry = accessor.entry(i).unwrap(); + let collection: &UntypedDynamicCollection = + UntypedDynamicCollection::new(entry.value()); + match collection.collection_type() { + Inline => { + let inline_accessor = LeafAccessor::new( + collection.as_inline(), + fixed_value_size, + <() as Value>::fixed_width(), + ); + leaf_bytes += + inline_accessor.length_of_pairs(0, inline_accessor.num_pairs()) as u64; + } + SubtreeV2 => { + is_branch = true; + } + } + } + let mut overhead_bytes = (accessor.total_length() as u64) - leaf_bytes; + let mut fragmented_bytes = (page.memory().len() - accessor.total_length()) as u64; + let mut max_child_height = 0; + let (mut leaf_pages, mut branch_pages) = if is_branch { (0, 1) } else { (1, 0) }; + + for i in 0..accessor.num_pairs() { + let entry = accessor.entry(i).unwrap(); + let collection: &UntypedDynamicCollection = + UntypedDynamicCollection::new(entry.value()); + match collection.collection_type() { + Inline => { + // data is inline, so it was already counted above + } + SubtreeV2 => { + // this is a sub-tree, so traverse it + let stats = btree_stats( + Some(collection.as_subtree().root), + mem, + fixed_value_size, + <() as Value>::fixed_width(), + hint, + )?; + max_child_height = max(max_child_height, stats.tree_height); + branch_pages += stats.branch_pages; + leaf_pages += stats.leaf_pages; + fragmented_bytes += stats.fragmented_bytes; + overhead_bytes += stats.metadata_bytes; + leaf_bytes += stats.stored_leaf_bytes; + } + } + } + + Ok(BtreeStats { + tree_height: max_child_height + 1, + leaf_pages, + branch_pages, + stored_leaf_bytes: leaf_bytes, + metadata_bytes: overhead_bytes, + fragmented_bytes, + }) + } + BRANCH => { + let accessor = BranchAccessor::new(&page, fixed_key_size); + let mut max_child_height = 0; + let mut leaf_pages = 0; + let mut branch_pages = 1; + let mut stored_leaf_bytes = 0; + let mut metadata_bytes = accessor.total_length() as u64; + let mut fragmented_bytes = (page.memory().len() - accessor.total_length()) as u64; + for i in 0..accessor.count_children() { + if let Some(child) = accessor.child_page(i) { + let stats = + multimap_stats_helper(child, mem, fixed_key_size, fixed_value_size, hint)?; + max_child_height = max(max_child_height, stats.tree_height); + leaf_pages += stats.leaf_pages; + branch_pages += stats.branch_pages; + stored_leaf_bytes += stats.stored_leaf_bytes; + metadata_bytes += stats.metadata_bytes; + fragmented_bytes += stats.fragmented_bytes; + } + } + + Ok(BtreeStats { + tree_height: max_child_height + 1, + leaf_pages, + branch_pages, + stored_leaf_bytes, + metadata_bytes, + fragmented_bytes, + }) + } + _ => unreachable!(), + } +} + +// Verify all the checksums in the tree, including any Dynamic collection subtrees +pub(crate) fn verify_tree_and_subtree_checksums( + root: Option, + key_size: Option, + value_size: Option, + mem: Arc, + hint: PageHint, +) -> Result { + if let Some(header) = root { + if !RawBtree::new( + Some(header), + key_size, + DynamicCollection::<()>::fixed_width_with(value_size), + mem.clone(), + hint, + ) + .verify_checksum()? + { + return Ok(false); + } + + let table_pages_iter = AllPageNumbersBtreeIter::new( + header.root, + key_size, + DynamicCollection::<()>::fixed_width_with(value_size), + mem.clone(), + hint, + )?; + for table_page in table_pages_iter { + let page = mem.get_page(table_page?, hint)?; + let subtree_roots = parse_subtree_roots(&page, key_size, value_size); + for header in subtree_roots { + if !RawBtree::new( + Some(header), + value_size, + <()>::fixed_width(), + mem.clone(), + hint, + ) + .verify_checksum()? + { + return Ok(false); + } + } + } + } + + Ok(true) +} + +// Relocate all subtrees to lower index pages, if possible +pub(crate) fn relocate_subtrees( + root: (PageNumber, Checksum), + key_size: Option, + value_size: Option, + mem: Arc, + freed_pages: Arc>>, + relocation_map: &HashMap, +) -> Result<(PageNumber, Checksum)> { + let old_page = mem.get_page(root.0, PageHint::None)?; + let mut new_page = if let Some(new_page_number) = relocation_map.get(&root.0) { + mem.get_page_mut(*new_page_number)? + } else { + return Ok(root); + }; + let new_page_number = new_page.get_page_number(); + new_page.memory_mut().copy_from_slice(old_page.memory()); + + match old_page.memory()[0] { + LEAF => { + let accessor = LeafAccessor::new( + old_page.memory(), + key_size, + UntypedDynamicCollection::fixed_width_with(value_size), + ); + // TODO: maybe there's a better abstraction, so that we don't need to call into this low-level method? + let mut mutator = LeafMutator::new( + new_page.memory_mut(), + key_size, + UntypedDynamicCollection::fixed_width_with(value_size), + ); + for i in 0..accessor.num_pairs() { + let entry = accessor.entry(i).unwrap(); + let collection = UntypedDynamicCollection::from_bytes(entry.value()); + if matches!(collection.collection_type(), SubtreeV2) { + let sub_root = collection.as_subtree(); + let mut tree = UntypedBtreeMut::new( + Some(sub_root), + mem.clone(), + freed_pages.clone(), + value_size, + <() as Value>::fixed_width(), + ); + tree.relocate(relocation_map)?; + if sub_root != tree.get_root().unwrap() { + let new_collection = + UntypedDynamicCollection::make_subtree_data(tree.get_root().unwrap()); + mutator.replace(i, &new_collection); + } + } + } + } + BRANCH => { + let accessor = BranchAccessor::new(&old_page, key_size); + let mut mutator = BranchMutator::new(new_page.memory_mut()); + for i in 0..accessor.count_children() { + if let Some(child) = accessor.child_page(i) { + let child_checksum = accessor.child_checksum(i).unwrap(); + let (new_child, new_checksum) = relocate_subtrees( + (child, child_checksum), + key_size, + value_size, + mem.clone(), + freed_pages.clone(), + relocation_map, + )?; + mutator.write_child_page(i, new_child, new_checksum); + } + } + } + _ => unreachable!(), + } + + let old_page_number = old_page.get_page_number(); + drop(old_page); + // No need to track allocations, because this method is only called during compaction when + // there can't be any savepoints + let mut ignore = PageTrackerPolicy::Ignore; + if !mem.free_if_uncommitted(old_page_number, &mut ignore) { + freed_pages.lock().unwrap().push(old_page_number); + } + Ok((new_page_number, DEFERRED)) +} + +// Finalize all the checksums in the tree, including any Dynamic collection subtrees +// Returns the root checksum +pub(crate) fn finalize_tree_and_subtree_checksums( + root: Option, + key_size: Option, + value_size: Option, + mem: Arc, +) -> Result> { + let freed_pages = Arc::new(Mutex::new(vec![])); + let mut tree = UntypedBtreeMut::new( + root, + mem.clone(), + freed_pages.clone(), + key_size, + DynamicCollection::<()>::fixed_width_with(value_size), + ); + tree.dirty_leaf_visitor(|mut leaf_page| { + let mut sub_root_updates = vec![]; + let accessor = LeafAccessor::new( + leaf_page.memory(), + key_size, + DynamicCollection::<()>::fixed_width_with(value_size), + ); + for i in 0..accessor.num_pairs() { + let entry = accessor.entry(i).unwrap(); + let collection = <&DynamicCollection<()>>::from_bytes(entry.value()); + if matches!(collection.collection_type(), SubtreeV2) { + let sub_root = collection.as_subtree(); + if mem.uncommitted(sub_root.root) { + let mut subtree = UntypedBtreeMut::new( + Some(sub_root), + mem.clone(), + freed_pages.clone(), + value_size, + <()>::fixed_width(), + ); + let subtree_root = subtree.finalize_dirty_checksums()?.unwrap(); + sub_root_updates.push((i, subtree_root)); + } + } + } + // TODO: maybe there's a better abstraction, so that we don't need to call into this low-level method? + let mut mutator = LeafMutator::new( + leaf_page.memory_mut(), + key_size, + DynamicCollection::<()>::fixed_width_with(value_size), + ); + for (i, sub_root) in sub_root_updates { + let collection = DynamicCollection::<()>::make_subtree_data(sub_root); + mutator.replace(i, &collection); + } + + Ok(()) + })?; + + let root = tree.finalize_dirty_checksums()?; + // No pages should have been freed by this operation + assert!(freed_pages.lock().unwrap().is_empty()); + Ok(root) +} + +fn parse_subtree_roots( + page: &T, + fixed_key_size: Option, + fixed_value_size: Option, +) -> Vec { + match page.memory()[0] { + BRANCH => { + vec![] + } + LEAF => { + let mut result = vec![]; + let accessor = LeafAccessor::new( + page.memory(), + fixed_key_size, + DynamicCollection::<()>::fixed_width_with(fixed_value_size), + ); + for i in 0..accessor.num_pairs() { + let entry = accessor.entry(i).unwrap(); + let collection = <&DynamicCollection<()>>::from_bytes(entry.value()); + if matches!(collection.collection_type(), SubtreeV2) { + result.push(collection.as_subtree()); + } + } + + result + } + _ => unreachable!(), + } +} + +pub(crate) struct UntypedMultiBtree { + mem: Arc, + root: Option, + hint: PageHint, + key_width: Option, + value_width: Option, +} + +impl UntypedMultiBtree { + pub(crate) fn new( + root: Option, + mem: Arc, + hint: PageHint, + key_width: Option, + value_width: Option, + ) -> Self { + Self { + mem, + root, + hint, + key_width, + value_width, + } + } + + // Applies visitor to pages in the tree + pub(crate) fn visit_all_pages(&self, mut visitor: F) -> Result + where + F: FnMut(&PagePath) -> Result, + { + let tree = UntypedBtree::new( + self.root, + self.mem.clone(), + self.hint, + self.key_width, + UntypedDynamicCollection::fixed_width_with(self.value_width), + ); + tree.visit_all_pages(|path| { + visitor(path)?; + let page = self.mem.get_page(path.page_number(), self.hint)?; + match page.memory()[0] { + LEAF => { + for header in parse_subtree_roots(&page, self.key_width, self.value_width) { + let subtree = UntypedBtree::new( + Some(header), + self.mem.clone(), + self.hint, + self.value_width, + <() as Value>::fixed_width(), + ); + subtree.visit_all_pages(|subpath| { + let full_path = path.with_subpath(subpath); + visitor(&full_path) + })?; + } + } + BRANCH => { + // No-op. The tree.visit_pages() call will process this sub-tree + } + _ => unreachable!(), + } + Ok(()) + })?; + + Ok(()) + } +} + +pub(crate) struct LeafKeyIter<'a, V: Key + 'static> { + inline_collection: AccessGuard<'a, &'static DynamicCollection>, + fixed_key_size: Option, + fixed_value_size: Option, + start_entry: isize, // inclusive + end_entry: isize, // inclusive +} + +impl<'a, V: Key> LeafKeyIter<'a, V> { + fn new( + data: AccessGuard<'a, &'static DynamicCollection>, + fixed_key_size: Option, + fixed_value_size: Option, + ) -> Self { + let accessor = + LeafAccessor::new(data.value().as_inline(), fixed_key_size, fixed_value_size); + let end_entry = isize::try_from(accessor.num_pairs()).unwrap() - 1; + Self { + inline_collection: data, + fixed_key_size, + fixed_value_size, + start_entry: 0, + end_entry, + } + } + + fn next_key(&mut self) -> Option<&[u8]> { + if self.end_entry < self.start_entry { + return None; + } + let accessor = LeafAccessor::new( + self.inline_collection.value().as_inline(), + self.fixed_key_size, + self.fixed_value_size, + ); + self.start_entry += 1; + accessor + .entry((self.start_entry - 1).try_into().unwrap()) + .map(|e| e.key()) + } + + fn next_key_back(&mut self) -> Option<&[u8]> { + if self.end_entry < self.start_entry { + return None; + } + let accessor = LeafAccessor::new( + self.inline_collection.value().as_inline(), + self.fixed_key_size, + self.fixed_value_size, + ); + self.end_entry -= 1; + accessor + .entry((self.end_entry + 1).try_into().unwrap()) + .map(|e| e.key()) + } +} + +enum DynamicCollectionType { + Inline, + // Was used in file format version 1 + // Subtree, + SubtreeV2, +} + +impl From for DynamicCollectionType { + fn from(value: u8) -> Self { + match value { + LEAF => Inline, + // 2 => Subtree, + 3 => SubtreeV2, + _ => unreachable!(), + } + } +} + +#[allow(clippy::from_over_into)] +impl Into for DynamicCollectionType { + fn into(self) -> u8 { + match self { + // Reuse the LEAF type id, so that we can cast this directly into the format used by + // LeafAccessor + Inline => LEAF, + // Subtree => 2, + SubtreeV2 => 3, + } + } +} + +/// Layout: +/// type (1 byte): +/// * 1 = inline data +/// * 2 = sub tree +/// +/// (when type = 1) data (n bytes): inlined leaf node +/// +/// (when type = 2) root (8 bytes): sub tree root page number +/// (when type = 2) checksum (16 bytes): sub tree checksum +/// +/// NOTE: Even though the [`PhantomData`] is zero-sized, the inner data DST must be placed last. +/// See [Exotically Sized Types](https://doc.rust-lang.org/nomicon/exotic-sizes.html#dynamically-sized-types-dsts) +/// section of the Rustonomicon for more details. +#[repr(transparent)] +struct DynamicCollection { + _value_type: PhantomData, + data: [u8], +} + +impl std::fmt::Debug for DynamicCollection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DynamicCollection") + .field("data", &&self.data) + .finish() + } +} + +impl Value for &DynamicCollection { + type SelfType<'a> + = &'a DynamicCollection + where + Self: 'a; + type AsBytes<'a> + = &'a [u8] + where + Self: 'a; + + fn fixed_width() -> Option { + None + } + + fn from_bytes<'a>(data: &'a [u8]) -> &'a DynamicCollection + where + Self: 'a, + { + DynamicCollection::new(data) + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> &'a [u8] + where + Self: 'b, + { + &value.data + } + + fn type_name() -> TypeName { + TypeName::internal("redb::DynamicCollection") + } +} + +impl DynamicCollection { + fn new(data: &[u8]) -> &Self { + unsafe { &*(std::ptr::from_ref::<[u8]>(data) as *const DynamicCollection) } + } + + fn collection_type(&self) -> DynamicCollectionType { + DynamicCollectionType::from(self.data[0]) + } + + fn as_inline(&self) -> &[u8] { + debug_assert!(matches!(self.collection_type(), Inline)); + &self.data[1..] + } + + fn as_subtree(&self) -> BtreeHeader { + assert!(matches!(self.collection_type(), SubtreeV2)); + BtreeHeader::from_le_bytes( + self.data[1..=BtreeHeader::serialized_size()] + .try_into() + .unwrap(), + ) + } + + fn get_num_values(&self) -> u64 { + match self.collection_type() { + Inline => { + let leaf_data = self.as_inline(); + let accessor = + LeafAccessor::new(leaf_data, V::fixed_width(), <() as Value>::fixed_width()); + accessor.num_pairs() as u64 + } + SubtreeV2 => { + let offset = 1 + PageNumber::serialized_size() + size_of::(); + u64::from_le_bytes( + self.data[offset..(offset + size_of::())] + .try_into() + .unwrap(), + ) + } + } + } + + fn make_inline_data(data: &[u8]) -> Vec { + let mut result = vec![Inline.into()]; + result.extend_from_slice(data); + + result + } + + fn make_subtree_data(header: BtreeHeader) -> Vec { + let mut result = vec![SubtreeV2.into()]; + result.extend_from_slice(&header.to_le_bytes()); + result + } + + pub(crate) fn fixed_width_with(_value_width: Option) -> Option { + None + } +} + +impl DynamicCollection { + fn iter<'a>( + collection: AccessGuard<'a, &'static DynamicCollection>, + guard: Arc, + mem: Arc, + ) -> Result> { + Ok(match collection.value().collection_type() { + Inline => { + let leaf_iter = + LeafKeyIter::new(collection, V::fixed_width(), <() as Value>::fixed_width()); + MultimapValue::new_inline(leaf_iter, guard) + } + SubtreeV2 => { + let root = collection.value().as_subtree().root; + MultimapValue::new_subtree( + BtreeRangeIter::new::>( + &(..), + Some(root), + mem, + PageHint::None, + )?, + collection.value().get_num_values(), + guard, + ) + } + }) + } + + fn iter_free_on_drop<'a>( + collection: AccessGuard<'a, &'static DynamicCollection>, + pages: Vec, + freed_pages: Arc>>, + allocated_pages: Arc>, + guard: Arc, + mem: Arc, + ) -> Result> { + let num_values = collection.value().get_num_values(); + Ok(match collection.value().collection_type() { + Inline => { + let leaf_iter = + LeafKeyIter::new(collection, V::fixed_width(), <() as Value>::fixed_width()); + MultimapValue::new_inline(leaf_iter, guard) + } + SubtreeV2 => { + let root = collection.value().as_subtree().root; + let inner = BtreeRangeIter::new::>( + &(..), + Some(root), + mem.clone(), + PageHint::None, + )?; + MultimapValue::new_subtree_free_on_drop( + inner, + num_values, + freed_pages, + allocated_pages, + pages, + guard, + mem, + ) + } + }) + } +} + +#[repr(transparent)] +struct UntypedDynamicCollection { + data: [u8], +} + +impl UntypedDynamicCollection { + pub(crate) fn fixed_width_with(_value_width: Option) -> Option { + None + } + + fn new(data: &[u8]) -> &Self { + unsafe { &*(std::ptr::from_ref::<[u8]>(data) as *const UntypedDynamicCollection) } + } + + fn make_subtree_data(header: BtreeHeader) -> Vec { + let mut result = vec![SubtreeV2.into()]; + result.extend_from_slice(&header.to_le_bytes()); + result + } + + fn from_bytes(data: &[u8]) -> &Self { + Self::new(data) + } + + fn collection_type(&self) -> DynamicCollectionType { + DynamicCollectionType::from(self.data[0]) + } + + fn as_inline(&self) -> &[u8] { + debug_assert!(matches!(self.collection_type(), Inline)); + &self.data[1..] + } + + fn as_subtree(&self) -> BtreeHeader { + assert!(matches!(self.collection_type(), SubtreeV2)); + BtreeHeader::from_le_bytes( + self.data[1..=BtreeHeader::serialized_size()] + .try_into() + .unwrap(), + ) + } +} + +enum ValueIterState<'a, V: Key + 'static> { + Subtree(BtreeRangeIter), + InlineLeaf(LeafKeyIter<'a, V>), +} + +pub struct MultimapValue<'a, V: Key + 'static> { + inner: Option>, + remaining: u64, + freed_pages: Option>>>, + allocated_pages: Arc>, + free_on_drop: Vec, + _transaction_guard: Arc, + mem: Option>, + _value_type: PhantomData, +} + +impl<'a, V: Key + 'static> MultimapValue<'a, V> { + fn new_subtree( + inner: BtreeRangeIter, + num_values: u64, + guard: Arc, + ) -> Self { + Self { + inner: Some(ValueIterState::Subtree(inner)), + remaining: num_values, + freed_pages: None, + allocated_pages: Arc::new(Mutex::new(PageTrackerPolicy::Closed)), + free_on_drop: vec![], + _transaction_guard: guard, + mem: None, + _value_type: Default::default(), + } + } + + fn new_subtree_free_on_drop( + inner: BtreeRangeIter, + num_values: u64, + freed_pages: Arc>>, + allocated_pages: Arc>, + pages: Vec, + guard: Arc, + mem: Arc, + ) -> Self { + Self { + inner: Some(ValueIterState::Subtree(inner)), + remaining: num_values, + freed_pages: Some(freed_pages), + free_on_drop: pages, + allocated_pages, + _transaction_guard: guard, + mem: Some(mem), + _value_type: Default::default(), + } + } + + fn new_inline(inner: LeafKeyIter<'a, V>, guard: Arc) -> Self { + let remaining = inner.inline_collection.value().get_num_values(); + Self { + inner: Some(ValueIterState::InlineLeaf(inner)), + remaining, + freed_pages: None, + allocated_pages: Arc::new(Mutex::new(PageTrackerPolicy::Closed)), + free_on_drop: vec![], + _transaction_guard: guard, + mem: None, + _value_type: Default::default(), + } + } + + /// Returns the number of times this iterator will return `Some(Ok(_))` + /// + /// Note that `Some` may be returned from `next()` more than `len()` times if `Some(Err(_))` is returned + pub fn len(&self) -> u64 { + self.remaining + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +impl<'a, V: Key + 'static> Iterator for MultimapValue<'a, V> { + type Item = Result>; + + fn next(&mut self) -> Option { + // TODO: optimize out this copy + let bytes = match self.inner.as_mut().unwrap() { + ValueIterState::Subtree(iter) => match iter.next()? { + Ok(e) => e.key_data(), + Err(err) => { + return Some(Err(err)); + } + }, + ValueIterState::InlineLeaf(iter) => iter.next_key()?.to_vec(), + }; + self.remaining -= 1; + Some(Ok(AccessGuard::with_owned_value(bytes))) + } +} + +impl DoubleEndedIterator for MultimapValue<'_, V> { + fn next_back(&mut self) -> Option { + // TODO: optimize out this copy + let bytes = match self.inner.as_mut().unwrap() { + ValueIterState::Subtree(iter) => match iter.next_back()? { + Ok(e) => e.key_data(), + Err(err) => { + return Some(Err(err)); + } + }, + ValueIterState::InlineLeaf(iter) => iter.next_key_back()?.to_vec(), + }; + self.remaining -= 1; + Some(Ok(AccessGuard::with_owned_value(bytes))) + } +} + +impl Drop for MultimapValue<'_, V> { + fn drop(&mut self) { + // Drop our references to the pages that are about to be freed + drop(mem::take(&mut self.inner)); + if !self.free_on_drop.is_empty() { + let mut freed_pages = self.freed_pages.as_ref().unwrap().lock().unwrap(); + let mut allocated_pages = self.allocated_pages.lock().unwrap(); + for page in &self.free_on_drop { + if !self + .mem + .as_ref() + .unwrap() + .free_if_uncommitted(*page, &mut allocated_pages) + { + freed_pages.push(*page); + } + } + } + } +} + +pub struct MultimapRange<'a, K: Key + 'static, V: Key + 'static> { + inner: BtreeRangeIter>, + mem: Arc, + transaction_guard: Arc, + _key_type: PhantomData, + _value_type: PhantomData, + _lifetime: PhantomData<&'a ()>, +} + +impl MultimapRange<'_, K, V> { + fn new( + inner: BtreeRangeIter>, + guard: Arc, + mem: Arc, + ) -> Self { + Self { + inner, + mem, + transaction_guard: guard, + _key_type: Default::default(), + _value_type: Default::default(), + _lifetime: Default::default(), + } + } +} + +impl<'a, K: Key + 'static, V: Key + 'static> Iterator for MultimapRange<'a, K, V> { + type Item = Result<(AccessGuard<'a, K>, MultimapValue<'a, V>)>; + + fn next(&mut self) -> Option { + match self.inner.next()? { + Ok(entry) => { + let key = AccessGuard::with_owned_value(entry.key_data()); + let (page, _, value_range) = entry.into_raw(); + let collection = AccessGuard::with_page(page, value_range); + Some( + DynamicCollection::iter( + collection, + self.transaction_guard.clone(), + self.mem.clone(), + ) + .map(|iter| (key, iter)), + ) + } + Err(err) => Some(Err(err)), + } + } +} + +impl DoubleEndedIterator for MultimapRange<'_, K, V> { + fn next_back(&mut self) -> Option { + match self.inner.next_back()? { + Ok(entry) => { + let key = AccessGuard::with_owned_value(entry.key_data()); + let (page, _, value_range) = entry.into_raw(); + let collection = AccessGuard::with_page(page, value_range); + Some( + DynamicCollection::iter( + collection, + self.transaction_guard.clone(), + self.mem.clone(), + ) + .map(|iter| (key, iter)), + ) + } + Err(err) => Some(Err(err)), + } + } +} + +/// A multimap table +/// +/// [Multimap tables](https://en.wikipedia.org/wiki/Multimap) may have multiple values associated with each key +pub struct MultimapTable<'txn, K: Key + 'static, V: Key + 'static> { + name: String, + num_values: u64, + transaction: &'txn WriteTransaction, + freed_pages: Arc>>, + allocated_pages: Arc>, + tree: BtreeMut<'txn, K, &'static DynamicCollection>, + mem: Arc, + _value_type: PhantomData, +} + +impl MultimapTableHandle for MultimapTable<'_, K, V> { + fn name(&self) -> &str { + &self.name + } +} + +impl<'txn, K: Key + 'static, V: Key + 'static> MultimapTable<'txn, K, V> { + pub(crate) fn new( + name: &str, + table_root: Option, + num_values: u64, + freed_pages: Arc>>, + allocated_pages: Arc>, + mem: Arc, + transaction: &'txn WriteTransaction, + ) -> MultimapTable<'txn, K, V> { + MultimapTable { + name: name.to_string(), + num_values, + transaction, + freed_pages: freed_pages.clone(), + allocated_pages: allocated_pages.clone(), + tree: BtreeMut::new( + table_root, + transaction.transaction_guard(), + mem.clone(), + freed_pages, + allocated_pages, + ), + mem, + _value_type: Default::default(), + } + } + + #[allow(dead_code)] + pub(crate) fn print_debug(&self, include_values: bool) -> Result { + self.tree.print_debug(include_values) + } + + /// Add the given value to the mapping of the key + /// + /// Returns `true` if the key-value pair was present + pub fn insert<'k, 'v>( + &mut self, + key: impl Borrow>, + value: impl Borrow>, + ) -> Result { + let value_bytes = V::as_bytes(value.borrow()); + let value_bytes_ref = value_bytes.as_ref(); + if value_bytes_ref.len() > MAX_VALUE_LENGTH { + return Err(StorageError::ValueTooLarge(value_bytes_ref.len())); + } + let key_len = K::as_bytes(key.borrow()).as_ref().len(); + if key_len > MAX_VALUE_LENGTH { + return Err(StorageError::ValueTooLarge(key_len)); + } + if value_bytes_ref.len() + key_len > MAX_PAIR_LENGTH { + return Err(StorageError::ValueTooLarge(value_bytes_ref.len() + key_len)); + } + let get_result = self.tree.get(key.borrow())?; + let existed = if get_result.is_some() { + #[allow(clippy::unnecessary_unwrap)] + let guard = get_result.unwrap(); + let collection_type = guard.value().collection_type(); + match collection_type { + Inline => { + let leaf_data = guard.value().as_inline(); + let accessor = LeafAccessor::new( + leaf_data, + V::fixed_width(), + <() as Value>::fixed_width(), + ); + let (position, found) = accessor.position::(value_bytes_ref); + if found { + return Ok(true); + } + + let num_pairs = accessor.num_pairs(); + let new_pairs = num_pairs + 1; + let new_pair_bytes = + accessor.length_of_pairs(0, accessor.num_pairs()) + value_bytes_ref.len(); + let new_key_bytes = + accessor.length_of_keys(0, accessor.num_pairs()) + value_bytes_ref.len(); + let required_inline_bytes = RawLeafBuilder::required_bytes( + new_pairs, + new_pair_bytes, + V::fixed_width(), + <() as Value>::fixed_width(), + ); + + if required_inline_bytes < self.mem.get_page_size() / 2 { + let mut data = vec![0; required_inline_bytes]; + let mut builder = RawLeafBuilder::new( + &mut data, + new_pairs, + V::fixed_width(), + <() as Value>::fixed_width(), + new_key_bytes, + ); + for i in 0..accessor.num_pairs() { + if i == position { + builder + .append(value_bytes_ref, <() as Value>::as_bytes(&()).as_ref()); + } + let entry = accessor.entry(i).unwrap(); + builder.append(entry.key(), entry.value()); + } + if position == accessor.num_pairs() { + builder.append(value_bytes_ref, <() as Value>::as_bytes(&()).as_ref()); + } + drop(builder); + drop(guard); + let inline_data = DynamicCollection::::make_inline_data(&data); + self.tree + .insert(key.borrow(), &DynamicCollection::new(&inline_data))?; + } else { + // convert into a subtree + let mut allocated = self.allocated_pages.lock().unwrap(); + let mut page = self.mem.allocate(leaf_data.len(), &mut allocated)?; + drop(allocated); + page.memory_mut()[..leaf_data.len()].copy_from_slice(leaf_data); + let page_number = page.get_page_number(); + drop(page); + drop(guard); + + // Don't bother computing the checksum, since we're about to modify the tree + let mut subtree: BtreeMut<'_, V, ()> = BtreeMut::new( + Some(BtreeHeader::new(page_number, 0, num_pairs as u64)), + self.transaction.transaction_guard(), + self.mem.clone(), + self.freed_pages.clone(), + self.allocated_pages.clone(), + ); + let existed = subtree.insert(value.borrow(), &())?.is_some(); + assert_eq!(existed, found); + let subtree_data = + DynamicCollection::::make_subtree_data(subtree.get_root().unwrap()); + self.tree + .insert(key.borrow(), &DynamicCollection::new(&subtree_data))?; + } + + found + } + SubtreeV2 => { + let mut subtree: BtreeMut<'_, V, ()> = BtreeMut::new( + Some(guard.value().as_subtree()), + self.transaction.transaction_guard(), + self.mem.clone(), + self.freed_pages.clone(), + self.allocated_pages.clone(), + ); + drop(guard); + let existed = subtree.insert(value.borrow(), &())?.is_some(); + let subtree_data = + DynamicCollection::::make_subtree_data(subtree.get_root().unwrap()); + self.tree + .insert(key.borrow(), &DynamicCollection::new(&subtree_data))?; + + existed + } + } + } else { + drop(get_result); + let required_inline_bytes = RawLeafBuilder::required_bytes( + 1, + value_bytes_ref.len(), + V::fixed_width(), + <() as Value>::fixed_width(), + ); + if required_inline_bytes < self.mem.get_page_size() / 2 { + let mut data = vec![0; required_inline_bytes]; + let mut builder = RawLeafBuilder::new( + &mut data, + 1, + V::fixed_width(), + <() as Value>::fixed_width(), + value_bytes_ref.len(), + ); + builder.append(value_bytes_ref, <() as Value>::as_bytes(&()).as_ref()); + drop(builder); + let inline_data = DynamicCollection::::make_inline_data(&data); + self.tree + .insert(key.borrow(), &DynamicCollection::new(&inline_data))?; + } else { + let mut subtree: BtreeMut<'_, V, ()> = BtreeMut::new( + None, + self.transaction.transaction_guard(), + self.mem.clone(), + self.freed_pages.clone(), + self.allocated_pages.clone(), + ); + subtree.insert(value.borrow(), &())?; + let subtree_data = + DynamicCollection::::make_subtree_data(subtree.get_root().unwrap()); + self.tree + .insert(key.borrow(), &DynamicCollection::new(&subtree_data))?; + } + false + }; + + if !existed { + self.num_values += 1; + } + + Ok(existed) + } + + /// Removes the given key-value pair + /// + /// Returns `true` if the key-value pair was present + pub fn remove<'k, 'v>( + &mut self, + key: impl Borrow>, + value: impl Borrow>, + ) -> Result { + let get_result = self.tree.get(key.borrow())?; + if get_result.is_none() { + return Ok(false); + } + let guard = get_result.unwrap(); + let v = guard.value(); + let existed = match v.collection_type() { + Inline => { + let leaf_data = v.as_inline(); + let accessor = + LeafAccessor::new(leaf_data, V::fixed_width(), <() as Value>::fixed_width()); + if let Some(position) = accessor.find_key::(V::as_bytes(value.borrow()).as_ref()) + { + let old_num_pairs = accessor.num_pairs(); + if old_num_pairs == 1 { + drop(guard); + self.tree.remove(key.borrow())?; + } else { + let old_pairs_len = accessor.length_of_pairs(0, old_num_pairs); + let removed_value_len = accessor.entry(position).unwrap().key().len(); + let required = RawLeafBuilder::required_bytes( + old_num_pairs - 1, + old_pairs_len - removed_value_len, + V::fixed_width(), + <() as Value>::fixed_width(), + ); + let mut new_data = vec![0; required]; + let new_key_len = + accessor.length_of_keys(0, old_num_pairs) - removed_value_len; + let mut builder = RawLeafBuilder::new( + &mut new_data, + old_num_pairs - 1, + V::fixed_width(), + <() as Value>::fixed_width(), + new_key_len, + ); + for i in 0..old_num_pairs { + if i != position { + let entry = accessor.entry(i).unwrap(); + builder.append(entry.key(), entry.value()); + } + } + drop(builder); + drop(guard); + + let inline_data = DynamicCollection::::make_inline_data(&new_data); + self.tree + .insert(key.borrow(), &DynamicCollection::new(&inline_data))?; + } + true + } else { + drop(guard); + false + } + } + SubtreeV2 => { + let mut subtree: BtreeMut = BtreeMut::new( + Some(v.as_subtree()), + self.transaction.transaction_guard(), + self.mem.clone(), + self.freed_pages.clone(), + self.allocated_pages.clone(), + ); + drop(guard); + let existed = subtree.remove(value.borrow())?.is_some(); + + if let Some(BtreeHeader { + root: new_root, + checksum: new_checksum, + length: new_length, + }) = subtree.get_root() + { + let page = self.mem.get_page(new_root, PageHint::None)?; + match page.memory()[0] { + LEAF => { + let accessor = LeafAccessor::new( + page.memory(), + V::fixed_width(), + <() as Value>::fixed_width(), + ); + let len = accessor.total_length(); + if len < self.mem.get_page_size() / 2 { + let inline_data = + DynamicCollection::::make_inline_data(&page.memory()[..len]); + self.tree + .insert(key.borrow(), &DynamicCollection::new(&inline_data))?; + drop(page); + let mut allocated_pages = self.allocated_pages.lock().unwrap(); + if !self.mem.free_if_uncommitted(new_root, &mut allocated_pages) { + (*self.freed_pages).lock().unwrap().push(new_root); + } + } else { + let subtree_data = + DynamicCollection::::make_subtree_data(BtreeHeader::new( + new_root, + new_checksum, + accessor.num_pairs() as u64, + )); + self.tree + .insert(key.borrow(), &DynamicCollection::new(&subtree_data))?; + } + } + BRANCH => { + let subtree_data = DynamicCollection::::make_subtree_data( + BtreeHeader::new(new_root, new_checksum, new_length), + ); + self.tree + .insert(key.borrow(), &DynamicCollection::new(&subtree_data))?; + } + _ => unreachable!(), + } + } else { + self.tree.remove(key.borrow())?; + } + + existed + } + }; + + if existed { + self.num_values -= 1; + } + + Ok(existed) + } + + /// Removes all values for the given key + /// + /// Returns an iterator over the removed values. Values are in ascending order. + pub fn remove_all<'a>( + &mut self, + key: impl Borrow>, + ) -> Result> { + let iter = if let Some(collection) = self.tree.remove(key.borrow())? { + let mut pages = vec![]; + if matches!( + collection.value().collection_type(), + DynamicCollectionType::SubtreeV2 + ) { + let root = collection.value().as_subtree().root; + let all_pages = AllPageNumbersBtreeIter::new( + root, + V::fixed_width(), + <() as Value>::fixed_width(), + self.mem.clone(), + PageHint::None, + )?; + for page in all_pages { + pages.push(page?); + } + } + + self.num_values -= collection.value().get_num_values(); + + DynamicCollection::iter_free_on_drop( + collection, + pages, + self.freed_pages.clone(), + self.allocated_pages.clone(), + self.transaction.transaction_guard(), + self.mem.clone(), + )? + } else { + MultimapValue::new_subtree( + BtreeRangeIter::new::>( + &(..), + None, + self.mem.clone(), + PageHint::None, + )?, + 0, + self.transaction.transaction_guard(), + ) + }; + + Ok(iter) + } +} + +impl ReadableTableMetadata for MultimapTable<'_, K, V> { + fn stats(&self) -> Result { + let tree_stats = multimap_btree_stats( + self.tree.get_root().map(|x| x.root), + &self.mem, + K::fixed_width(), + V::fixed_width(), + PageHint::None, + )?; + + Ok(TableStats { + tree_height: tree_stats.tree_height, + leaf_pages: tree_stats.leaf_pages, + branch_pages: tree_stats.branch_pages, + stored_leaf_bytes: tree_stats.stored_leaf_bytes, + metadata_bytes: tree_stats.metadata_bytes, + fragmented_bytes: tree_stats.fragmented_bytes, + }) + } + + /// Returns the number of key-value pairs in the table + fn len(&self) -> Result { + Ok(self.num_values) + } +} + +impl ReadableMultimapTable for MultimapTable<'_, K, V> { + fn get<'a>(&self, key: impl Borrow>) -> Result> { + let guard = self.transaction.transaction_guard(); + let iter = if let Some(collection) = self.tree.get(key.borrow())? { + DynamicCollection::iter(collection, guard, self.mem.clone())? + } else { + MultimapValue::new_subtree( + BtreeRangeIter::new::>( + &(..), + None, + self.mem.clone(), + PageHint::None, + )?, + 0, + guard, + ) + }; + + Ok(iter) + } + + fn range<'a, KR>(&self, range: impl RangeBounds + 'a) -> Result> + where + KR: Borrow> + 'a, + { + let inner = self.tree.range(&range)?; + Ok(MultimapRange::new( + inner, + self.transaction.transaction_guard(), + self.mem.clone(), + )) + } +} + +impl Sealed for MultimapTable<'_, K, V> {} + +impl Drop for MultimapTable<'_, K, V> { + fn drop(&mut self) { + self.transaction + .close_table(&self.name, &self.tree, self.num_values); + } +} + +pub trait ReadableMultimapTable: ReadableTableMetadata { + /// Returns an iterator over all values for the given key. Values are in ascending order. + fn get<'a>(&self, key: impl Borrow>) -> Result>; + + /// Returns a double-ended iterator over a range of elements in the table + fn range<'a, KR>(&self, range: impl RangeBounds + 'a) -> Result> + where + KR: Borrow> + 'a; + + /// Returns an double-ended iterator over all elements in the table. Values are in ascending + /// order. + fn iter(&self) -> Result> { + self.range::>(..) + } +} + +/// A read-only untyped multimap table +pub struct ReadOnlyUntypedMultimapTable { + num_values: u64, + tree: RawBtree, + hint: PageHint, + fixed_key_size: Option, + fixed_value_size: Option, + mem: Arc, +} + +impl Sealed for ReadOnlyUntypedMultimapTable {} + +impl ReadableTableMetadata for ReadOnlyUntypedMultimapTable { + /// Retrieves information about storage usage for the table + fn stats(&self) -> Result { + let tree_stats = multimap_btree_stats( + self.tree.get_root().map(|x| x.root), + &self.mem, + self.fixed_key_size, + self.fixed_value_size, + self.hint, + )?; + + Ok(TableStats { + tree_height: tree_stats.tree_height, + leaf_pages: tree_stats.leaf_pages, + branch_pages: tree_stats.branch_pages, + stored_leaf_bytes: tree_stats.stored_leaf_bytes, + metadata_bytes: tree_stats.metadata_bytes, + fragmented_bytes: tree_stats.fragmented_bytes, + }) + } + + fn len(&self) -> Result { + Ok(self.num_values) + } +} + +impl ReadOnlyUntypedMultimapTable { + pub(crate) fn new( + root: Option, + num_values: u64, + hint: PageHint, + fixed_key_size: Option, + fixed_value_size: Option, + mem: Arc, + ) -> Self { + Self { + num_values, + tree: RawBtree::new( + root, + fixed_key_size, + DynamicCollection::<()>::fixed_width_with(fixed_value_size), + mem.clone(), + hint, + ), + hint, + fixed_key_size, + fixed_value_size, + mem, + } + } +} + +/// A read-only multimap table +pub struct ReadOnlyMultimapTable { + tree: Btree>, + num_values: u64, + mem: Arc, + transaction_guard: Arc, + _value_type: PhantomData, +} + +impl ReadOnlyMultimapTable { + pub(crate) fn new( + root: Option, + num_values: u64, + hint: PageHint, + guard: Arc, + mem: Arc, + ) -> Result> { + Ok(ReadOnlyMultimapTable { + tree: Btree::new(root, hint, guard.clone(), mem.clone())?, + num_values, + mem, + transaction_guard: guard, + _value_type: Default::default(), + }) + } + + /// This method is like [`ReadableMultimapTable::get()`], but the iterator is reference counted and keeps the transaction + /// alive until it is dropped. + pub fn get<'a>(&self, key: impl Borrow>) -> Result> { + let iter = if let Some(collection) = self.tree.get(key.borrow())? { + DynamicCollection::iter(collection, self.transaction_guard.clone(), self.mem.clone())? + } else { + MultimapValue::new_subtree( + BtreeRangeIter::new::>( + &(..), + None, + self.mem.clone(), + PageHint::None, + )?, + 0, + self.transaction_guard.clone(), + ) + }; + + Ok(iter) + } + + /// This method is like [`ReadableMultimapTable::range()`], but the iterator is reference counted and keeps the transaction + /// alive until it is dropped. + pub fn range<'a, KR>(&self, range: impl RangeBounds) -> Result> + where + KR: Borrow>, + { + let inner = self.tree.range(&range)?; + Ok(MultimapRange::new( + inner, + self.transaction_guard.clone(), + self.mem.clone(), + )) + } +} + +impl ReadableTableMetadata for ReadOnlyMultimapTable { + fn stats(&self) -> Result { + let tree_stats = multimap_btree_stats( + self.tree.get_root().map(|x| x.root), + &self.mem, + K::fixed_width(), + V::fixed_width(), + self.tree.hint(), + )?; + + Ok(TableStats { + tree_height: tree_stats.tree_height, + leaf_pages: tree_stats.leaf_pages, + branch_pages: tree_stats.branch_pages, + stored_leaf_bytes: tree_stats.stored_leaf_bytes, + metadata_bytes: tree_stats.metadata_bytes, + fragmented_bytes: tree_stats.fragmented_bytes, + }) + } + + fn len(&self) -> Result { + Ok(self.num_values) + } +} + +impl ReadableMultimapTable + for ReadOnlyMultimapTable +{ + /// Returns an iterator over all values for the given key. Values are in ascending order. + fn get<'a>(&self, key: impl Borrow>) -> Result> { + let iter = if let Some(collection) = self.tree.get(key.borrow())? { + DynamicCollection::iter(collection, self.transaction_guard.clone(), self.mem.clone())? + } else { + MultimapValue::new_subtree( + BtreeRangeIter::new::>( + &(..), + None, + self.mem.clone(), + PageHint::None, + )?, + 0, + self.transaction_guard.clone(), + ) + }; + + Ok(iter) + } + + fn range<'a, KR>(&self, range: impl RangeBounds + 'a) -> Result> + where + KR: Borrow> + 'a, + { + let inner = self.tree.range(&range)?; + Ok(MultimapRange::new( + inner, + self.transaction_guard.clone(), + self.mem.clone(), + )) + } +} + +impl Sealed for ReadOnlyMultimapTable {} diff --git a/v2/redb/src/sealed.rs b/v2/redb/src/sealed.rs new file mode 100644 index 000000000..0650b2f3e --- /dev/null +++ b/v2/redb/src/sealed.rs @@ -0,0 +1 @@ +pub trait Sealed {} diff --git a/v2/redb/src/table.rs b/v2/redb/src/table.rs new file mode 100644 index 000000000..e8946bd0c --- /dev/null +++ b/v2/redb/src/table.rs @@ -0,0 +1,670 @@ +use crate::db::TransactionGuard; +use crate::sealed::Sealed; +use crate::tree_store::{ + AccessGuardMutInPlace, Btree, BtreeExtractIf, BtreeHeader, BtreeMut, BtreeRangeIter, + MAX_PAIR_LENGTH, MAX_VALUE_LENGTH, PageHint, PageNumber, PageTrackerPolicy, RawBtree, + TransactionalMemory, +}; +use crate::types::{Key, MutInPlaceValue, Value}; +use crate::{AccessGuard, AccessGuardMut, StorageError, WriteTransaction}; +use crate::{Result, TableHandle}; +use std::borrow::Borrow; +use std::fmt::{Debug, Formatter}; +use std::marker::PhantomData; +use std::ops::RangeBounds; +use std::sync::{Arc, Mutex}; + +/// Informational storage stats about a table +#[derive(Debug)] +pub struct TableStats { + pub(crate) tree_height: u32, + pub(crate) leaf_pages: u64, + pub(crate) branch_pages: u64, + pub(crate) stored_leaf_bytes: u64, + pub(crate) metadata_bytes: u64, + pub(crate) fragmented_bytes: u64, +} + +impl TableStats { + /// Maximum traversal distance to reach the deepest (key, value) pair in the table + pub fn tree_height(&self) -> u32 { + self.tree_height + } + + /// Number of leaf pages that store user data + pub fn leaf_pages(&self) -> u64 { + self.leaf_pages + } + + /// Number of branch pages in the btree that store user data + pub fn branch_pages(&self) -> u64 { + self.branch_pages + } + + /// Number of bytes consumed by keys and values that have been inserted. + /// Does not include indexing overhead + pub fn stored_bytes(&self) -> u64 { + self.stored_leaf_bytes + } + + /// Number of bytes consumed by keys in internal branch pages, plus other metadata + pub fn metadata_bytes(&self) -> u64 { + self.metadata_bytes + } + + /// Number of bytes consumed by fragmentation, both in data pages and internal metadata tables + pub fn fragmented_bytes(&self) -> u64 { + self.fragmented_bytes + } +} + +/// A table containing key-value mappings +pub struct Table<'txn, K: Key + 'static, V: Value + 'static> { + name: String, + transaction: &'txn WriteTransaction, + tree: BtreeMut<'txn, K, V>, +} + +impl TableHandle for Table<'_, K, V> { + fn name(&self) -> &str { + &self.name + } +} + +impl<'txn, K: Key + 'static, V: Value + 'static> Table<'txn, K, V> { + pub(crate) fn new( + name: &str, + table_root: Option, + freed_pages: Arc>>, + allocated_pages: Arc>, + mem: Arc, + transaction: &'txn WriteTransaction, + ) -> Table<'txn, K, V> { + Table { + name: name.to_string(), + transaction, + tree: BtreeMut::new( + table_root, + transaction.transaction_guard(), + mem, + freed_pages, + allocated_pages, + ), + } + } + + #[allow(dead_code)] + pub(crate) fn print_debug(&self, include_values: bool) -> Result { + self.tree.print_debug(include_values) + } + + /// Returns an accessor, which allows mutation, to the value corresponding to the given key + pub fn get_mut<'k>( + &mut self, + key: impl Borrow>, + ) -> Result>> { + self.tree.get_mut(key.borrow()) + } + + /// Removes and returns the first key-value pair in the table + pub fn pop_first(&mut self) -> Result, AccessGuard<'_, V>)>> { + // TODO: optimize this + let first = self + .iter()? + .next() + .map(|x| x.map(|(key, _)| K::as_bytes(&key.value()).as_ref().to_vec())); + if let Some(owned_key) = first { + let owned_key = owned_key?; + let key = K::from_bytes(&owned_key); + let value = self.remove(&key)?.unwrap(); + drop(key); + Ok(Some((AccessGuard::with_owned_value(owned_key), value))) + } else { + Ok(None) + } + } + + /// Removes and returns the last key-value pair in the table + pub fn pop_last(&mut self) -> Result, AccessGuard<'_, V>)>> { + // TODO: optimize this + let last = self + .iter()? + .next_back() + .map(|x| x.map(|(key, _)| K::as_bytes(&key.value()).as_ref().to_vec())); + if let Some(owned_key) = last { + let owned_key = owned_key?; + let key = K::from_bytes(&owned_key); + let value = self.remove(&key)?.unwrap(); + drop(key); + Ok(Some((AccessGuard::with_owned_value(owned_key), value))) + } else { + Ok(None) + } + } + + /// Applies `predicate` to all key-value pairs. All entries for which + /// `predicate` evaluates to `true` are returned in an iterator, and those which are read from the iterator are removed + /// + /// Note: values not read from the iterator will not be removed + pub fn extract_if FnMut(K::SelfType<'f>, V::SelfType<'f>) -> bool>( + &mut self, + predicate: F, + ) -> Result> { + self.extract_from_if::, F>(.., predicate) + } + + /// Applies `predicate` to all key-value pairs in the specified range. All entries for which + /// `predicate` evaluates to `true` are returned in an iterator, and those which are read from the iterator are removed + /// + /// Note: values not read from the iterator will not be removed + pub fn extract_from_if<'a, KR, F: for<'f> FnMut(K::SelfType<'f>, V::SelfType<'f>) -> bool>( + &mut self, + range: impl RangeBounds + 'a, + predicate: F, + ) -> Result> + where + KR: Borrow> + 'a, + { + self.tree + .extract_from_if(&range, predicate) + .map(ExtractIf::new) + } + + /// Applies `predicate` to all key-value pairs. All entries for which + /// `predicate` evaluates to `false` are removed. + /// + pub fn retain FnMut(K::SelfType<'f>, V::SelfType<'f>) -> bool>( + &mut self, + predicate: F, + ) -> Result { + self.tree.retain_in::, F>(predicate, ..) + } + + /// Applies `predicate` to all key-value pairs in the range `start..end`. All entries for which + /// `predicate` evaluates to `false` are removed. + /// + pub fn retain_in<'a, KR, F: for<'f> FnMut(K::SelfType<'f>, V::SelfType<'f>) -> bool>( + &mut self, + range: impl RangeBounds + 'a, + predicate: F, + ) -> Result + where + KR: Borrow> + 'a, + { + self.tree.retain_in(predicate, range) + } + + /// Insert mapping of the given key to the given value + /// + /// If key is already present it is replaced + /// + /// Returns the old value, if the key was present in the table, otherwise None is returned + pub fn insert<'k, 'v>( + &mut self, + key: impl Borrow>, + value: impl Borrow>, + ) -> Result>> { + let value_len = V::as_bytes(value.borrow()).as_ref().len(); + if value_len > MAX_VALUE_LENGTH { + return Err(StorageError::ValueTooLarge(value_len)); + } + let key_len = K::as_bytes(key.borrow()).as_ref().len(); + if key_len > MAX_VALUE_LENGTH { + return Err(StorageError::ValueTooLarge(key_len)); + } + if value_len + key_len > MAX_PAIR_LENGTH { + return Err(StorageError::ValueTooLarge(value_len + key_len)); + } + self.tree.insert(key.borrow(), value.borrow()) + } + + /// Removes the given key + /// + /// Returns the old value, if the key was present in the table + pub fn remove<'a>( + &mut self, + key: impl Borrow>, + ) -> Result>> { + self.tree.remove(key.borrow()) + } +} + +impl Table<'_, K, V> { + /// Reserve space to insert a key-value pair + /// + /// If key is already present it is replaced + /// + /// The returned reference will have length equal to `value_length` + pub fn insert_reserve<'a>( + &mut self, + key: impl Borrow>, + value_length: usize, + ) -> Result> { + if value_length > MAX_VALUE_LENGTH { + return Err(StorageError::ValueTooLarge(value_length)); + } + let key_len = K::as_bytes(key.borrow()).as_ref().len(); + if key_len > MAX_VALUE_LENGTH { + return Err(StorageError::ValueTooLarge(key_len)); + } + if value_length + key_len > MAX_PAIR_LENGTH { + return Err(StorageError::ValueTooLarge(value_length + key_len)); + } + self.tree.insert_reserve(key.borrow(), value_length) + } +} + +impl ReadableTableMetadata for Table<'_, K, V> { + fn stats(&self) -> Result { + let tree_stats = self.tree.stats()?; + + Ok(TableStats { + tree_height: tree_stats.tree_height, + leaf_pages: tree_stats.leaf_pages, + branch_pages: tree_stats.branch_pages, + stored_leaf_bytes: tree_stats.stored_leaf_bytes, + metadata_bytes: tree_stats.metadata_bytes, + fragmented_bytes: tree_stats.fragmented_bytes, + }) + } + + fn len(&self) -> Result { + self.tree.len() + } +} + +impl ReadableTable for Table<'_, K, V> { + fn get<'a>(&self, key: impl Borrow>) -> Result>> { + self.tree.get(key.borrow()) + } + + fn range<'a, KR>(&self, range: impl RangeBounds + 'a) -> Result> + where + KR: Borrow> + 'a, + { + self.tree + .range(&range) + .map(|x| Range::new(x, self.transaction.transaction_guard())) + } + + fn first(&self) -> Result, AccessGuard<'_, V>)>> { + self.tree.first() + } + + fn last(&self) -> Result, AccessGuard<'_, V>)>> { + self.tree.last() + } +} + +impl Sealed for Table<'_, K, V> {} + +impl Drop for Table<'_, K, V> { + fn drop(&mut self) { + self.transaction.close_table( + &self.name, + &self.tree, + self.tree.get_root().map(|x| x.length).unwrap_or_default(), + ); + } +} + +fn debug_helper( + f: &mut Formatter<'_>, + name: &str, + len: Result, + first: Result, AccessGuard)>>, + last: Result, AccessGuard)>>, +) -> std::fmt::Result { + write!(f, "Table [ name: \"{name}\", ")?; + if let Ok(len) = len { + if len == 0 { + write!(f, "No entries")?; + } else if len == 1 { + if let Ok(first) = first { + let (key, value) = first.as_ref().unwrap(); + write!(f, "One key-value: {:?} = {:?}", key.value(), value.value())?; + } else { + write!(f, "I/O Error accessing table!")?; + } + } else { + if let Ok(first) = first { + let (key, value) = first.as_ref().unwrap(); + write!(f, "first: {:?} = {:?}, ", key.value(), value.value())?; + } else { + write!(f, "I/O Error accessing table!")?; + } + if len > 2 { + write!(f, "...{} more entries..., ", len - 2)?; + } + if let Ok(last) = last { + let (key, value) = last.as_ref().unwrap(); + write!(f, "last: {:?} = {:?}", key.value(), value.value())?; + } else { + write!(f, "I/O Error accessing table!")?; + } + } + } else { + write!(f, "I/O Error accessing table!")?; + } + write!(f, " ]")?; + + Ok(()) +} + +impl Debug for Table<'_, K, V> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + debug_helper(f, &self.name, self.len(), self.first(), self.last()) + } +} + +pub trait ReadableTableMetadata { + /// Retrieves information about storage usage for the table + fn stats(&self) -> Result; + + /// Returns the number of entries in the table + fn len(&self) -> Result; + + /// Returns `true` if the table is empty + fn is_empty(&self) -> Result { + Ok(self.len()? == 0) + } +} + +pub trait ReadableTable: ReadableTableMetadata { + /// Returns the value corresponding to the given key + fn get<'a>(&self, key: impl Borrow>) -> Result>>; + + /// Returns a double-ended iterator over a range of elements in the table + /// + /// # Examples + /// + /// Usage: + /// ```rust + /// use redb::*; + /// # use tempfile::NamedTempFile; + /// const TABLE: TableDefinition<&str, u64> = TableDefinition::new("my_data"); + /// + /// # fn main() -> Result<(), Error> { + /// # #[cfg(not(target_os = "wasi"))] + /// # let tmpfile = NamedTempFile::new().unwrap(); + /// # #[cfg(target_os = "wasi")] + /// # let tmpfile = NamedTempFile::new_in("/tmp").unwrap(); + /// # let filename = tmpfile.path(); + /// let db = Database::create(filename)?; + /// let write_txn = db.begin_write()?; + /// { + /// let mut table = write_txn.open_table(TABLE)?; + /// table.insert("a", &0)?; + /// table.insert("b", &1)?; + /// table.insert("c", &2)?; + /// } + /// write_txn.commit()?; + /// + /// let read_txn = db.begin_read()?; + /// let table = read_txn.open_table(TABLE)?; + /// let mut iter = table.range("a".."c")?; + /// let (key, value) = iter.next().unwrap()?; + /// assert_eq!("a", key.value()); + /// assert_eq!(0, value.value()); + /// # Ok(()) + /// # } + /// ``` + fn range<'a, KR>(&self, range: impl RangeBounds + 'a) -> Result> + where + KR: Borrow> + 'a; + + /// Returns the first key-value pair in the table, if it exists + fn first(&self) -> Result, AccessGuard<'_, V>)>>; + + /// Returns the last key-value pair in the table, if it exists + fn last(&self) -> Result, AccessGuard<'_, V>)>>; + + /// Returns a double-ended iterator over all elements in the table + fn iter(&self) -> Result> { + self.range::>(..) + } +} + +/// A read-only untyped table +pub struct ReadOnlyUntypedTable { + tree: RawBtree, +} + +impl Sealed for ReadOnlyUntypedTable {} + +impl ReadableTableMetadata for ReadOnlyUntypedTable { + /// Retrieves information about storage usage for the table + fn stats(&self) -> Result { + let tree_stats = self.tree.stats()?; + + Ok(TableStats { + tree_height: tree_stats.tree_height, + leaf_pages: tree_stats.leaf_pages, + branch_pages: tree_stats.branch_pages, + stored_leaf_bytes: tree_stats.stored_leaf_bytes, + metadata_bytes: tree_stats.metadata_bytes, + fragmented_bytes: tree_stats.fragmented_bytes, + }) + } + + fn len(&self) -> Result { + self.tree.len() + } +} + +impl ReadOnlyUntypedTable { + pub(crate) fn new( + root_page: Option, + hint: PageHint, + fixed_key_size: Option, + fixed_value_size: Option, + mem: Arc, + ) -> Self { + Self { + tree: RawBtree::new(root_page, fixed_key_size, fixed_value_size, mem, hint), + } + } +} + +/// A read-only table +pub struct ReadOnlyTable { + name: String, + tree: Btree, + transaction_guard: Arc, +} + +impl TableHandle for ReadOnlyTable { + fn name(&self) -> &str { + &self.name + } +} + +impl ReadOnlyTable { + pub(crate) fn new( + name: String, + root_page: Option, + hint: PageHint, + guard: Arc, + mem: Arc, + ) -> Result> { + Ok(ReadOnlyTable { + name, + tree: Btree::new(root_page, hint, guard.clone(), mem)?, + transaction_guard: guard, + }) + } + + /// This method is like [`ReadableTable::get()`], but the [`AccessGuard`] is reference counted + /// and keeps the transaction alive until it is dropped. + pub fn get<'a>( + &self, + key: impl Borrow>, + ) -> Result>> { + self.tree.get(key.borrow()) + } + + /// This method is like [`ReadableTable::range()`], but the iterator is reference counted and keeps the transaction + /// alive until it is dropped. + pub fn range<'a, KR>(&self, range: impl RangeBounds) -> Result> + where + KR: Borrow>, + { + self.tree + .range(&range) + .map(|x| Range::new(x, self.transaction_guard.clone())) + } +} + +impl ReadableTableMetadata for ReadOnlyTable { + fn stats(&self) -> Result { + let tree_stats = self.tree.stats()?; + + Ok(TableStats { + tree_height: tree_stats.tree_height, + leaf_pages: tree_stats.leaf_pages, + branch_pages: tree_stats.branch_pages, + stored_leaf_bytes: tree_stats.stored_leaf_bytes, + metadata_bytes: tree_stats.metadata_bytes, + fragmented_bytes: tree_stats.fragmented_bytes, + }) + } + + fn len(&self) -> Result { + self.tree.len() + } +} + +impl ReadableTable for ReadOnlyTable { + fn get<'a>(&self, key: impl Borrow>) -> Result>> { + self.tree.get(key.borrow()) + } + + fn range<'a, KR>(&self, range: impl RangeBounds + 'a) -> Result> + where + KR: Borrow> + 'a, + { + self.tree + .range(&range) + .map(|x| Range::new(x, self.transaction_guard.clone())) + } + + fn first(&self) -> Result, AccessGuard<'_, V>)>> { + self.tree.first() + } + + fn last(&self) -> Result, AccessGuard<'_, V>)>> { + self.tree.last() + } +} + +impl Sealed for ReadOnlyTable {} + +impl Debug for ReadOnlyTable { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + debug_helper(f, &self.name, self.len(), self.first(), self.last()) + } +} + +pub struct ExtractIf< + 'a, + K: Key + 'static, + V: Value + 'static, + F: for<'f> FnMut(K::SelfType<'f>, V::SelfType<'f>) -> bool, +> { + inner: BtreeExtractIf<'a, K, V, F>, +} + +impl< + 'a, + K: Key + 'static, + V: Value + 'static, + F: for<'f> FnMut(K::SelfType<'f>, V::SelfType<'f>) -> bool, +> ExtractIf<'a, K, V, F> +{ + pub(crate) fn new(inner: BtreeExtractIf<'a, K, V, F>) -> Self { + Self { inner } + } +} + +impl< + 'a, + K: Key + 'static, + V: Value + 'static, + F: for<'f> FnMut(K::SelfType<'f>, V::SelfType<'f>) -> bool, +> Iterator for ExtractIf<'a, K, V, F> +{ + type Item = Result<(AccessGuard<'a, K>, AccessGuard<'a, V>)>; + + fn next(&mut self) -> Option { + let entry = self.inner.next()?; + Some(entry.map(|entry| { + let (page, key_range, value_range) = entry.into_raw(); + let key = AccessGuard::with_page(page.clone(), key_range); + let value = AccessGuard::with_page(page, value_range); + (key, value) + })) + } +} + +impl< + K: Key + 'static, + V: Value + 'static, + F: for<'f> FnMut(K::SelfType<'f>, V::SelfType<'f>) -> bool, +> DoubleEndedIterator for ExtractIf<'_, K, V, F> +{ + fn next_back(&mut self) -> Option { + let entry = self.inner.next_back()?; + Some(entry.map(|entry| { + let (page, key_range, value_range) = entry.into_raw(); + let key = AccessGuard::with_page(page.clone(), key_range); + let value = AccessGuard::with_page(page, value_range); + (key, value) + })) + } +} + +#[derive(Clone)] +pub struct Range<'a, K: Key + 'static, V: Value + 'static> { + inner: BtreeRangeIter, + _transaction_guard: Arc, + // This lifetime is here so that `&` can be held on `Table` preventing concurrent mutation + _lifetime: PhantomData<&'a ()>, +} + +impl Range<'_, K, V> { + pub(super) fn new(inner: BtreeRangeIter, guard: Arc) -> Self { + Self { + inner, + _transaction_guard: guard, + _lifetime: Default::default(), + } + } +} + +impl<'a, K: Key + 'static, V: Value + 'static> Iterator for Range<'a, K, V> { + type Item = Result<(AccessGuard<'a, K>, AccessGuard<'a, V>)>; + + fn next(&mut self) -> Option { + self.inner.next().map(|x| { + x.map(|entry| { + let (page, key_range, value_range) = entry.into_raw(); + let key = AccessGuard::with_page(page.clone(), key_range); + let value = AccessGuard::with_page(page, value_range); + (key, value) + }) + }) + } +} + +impl DoubleEndedIterator for Range<'_, K, V> { + fn next_back(&mut self) -> Option { + self.inner.next_back().map(|x| { + x.map(|entry| { + let (page, key_range, value_range) = entry.into_raw(); + let key = AccessGuard::with_page(page.clone(), key_range); + let value = AccessGuard::with_page(page, value_range); + (key, value) + }) + }) + } +} diff --git a/v2/redb/src/transaction_tracker.rs b/v2/redb/src/transaction_tracker.rs new file mode 100644 index 000000000..84e5d0603 --- /dev/null +++ b/v2/redb/src/transaction_tracker.rs @@ -0,0 +1,316 @@ +use crate::tree_store::TransactionalMemory; +use crate::{Key, Result, Savepoint, TypeName, Value}; +#[cfg(feature = "logging")] +use log::debug; +use std::cmp::Ordering; +use std::collections::btree_map::BTreeMap; +use std::collections::{BTreeSet, HashMap}; +use std::mem; +use std::mem::size_of; +use std::sync::{Condvar, Mutex}; + +#[derive(Copy, Clone, Hash, Ord, PartialOrd, Eq, PartialEq, Debug)] +pub(crate) struct TransactionId(u64); + +impl TransactionId { + pub(crate) fn new(value: u64) -> TransactionId { + Self(value) + } + + pub(crate) fn raw_id(self) -> u64 { + self.0 + } + + pub(crate) fn next(self) -> TransactionId { + TransactionId(self.0 + 1) + } + + pub(crate) fn increment(&mut self) -> TransactionId { + let next = self.next(); + *self = next; + next + } +} + +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)] +pub(crate) struct SavepointId(pub u64); + +impl SavepointId { + pub(crate) fn next(self) -> SavepointId { + SavepointId(self.0 + 1) + } +} + +impl Value for SavepointId { + type SelfType<'a> = SavepointId; + type AsBytes<'a> = [u8; size_of::()]; + + fn fixed_width() -> Option { + Some(size_of::()) + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + SavepointId(u64::from_le_bytes(data.try_into().unwrap())) + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'b, + { + value.0.to_le_bytes() + } + + fn type_name() -> TypeName { + TypeName::internal("redb::SavepointId") + } +} + +impl Key for SavepointId { + fn compare(data1: &[u8], data2: &[u8]) -> Ordering { + Self::from_bytes(data1).0.cmp(&Self::from_bytes(data2).0) + } +} + +struct State { + next_savepoint_id: SavepointId, + // reference count of read transactions per transaction id + live_read_transactions: BTreeMap, + next_transaction_id: TransactionId, + live_write_transaction: Option, + valid_savepoints: BTreeMap, + // Non-durable commits that are still in-memory, and waiting for a durable commit to get flushed + // We need to make sure that the freed-table does not get processed for these, since they are not durable yet + // Therefore, we hold a read transaction on their nearest durable ancestor + // + // Maps non-durable transaction id -> durable ancestor + pending_non_durable_commits: HashMap, + // Non-durable commits which have NOT been processed in the freed table + unprocessed_freed_non_durable_commits: BTreeSet, +} + +pub(crate) struct TransactionTracker { + state: Mutex, + live_write_transaction_available: Condvar, +} + +impl TransactionTracker { + pub(crate) fn new(next_transaction_id: TransactionId) -> Self { + Self { + state: Mutex::new(State { + next_savepoint_id: SavepointId(0), + live_read_transactions: Default::default(), + next_transaction_id, + live_write_transaction: None, + valid_savepoints: Default::default(), + pending_non_durable_commits: Default::default(), + unprocessed_freed_non_durable_commits: Default::default(), + }), + live_write_transaction_available: Condvar::new(), + } + } + + pub(crate) fn start_write_transaction(&self) -> TransactionId { + let mut state = self.state.lock().unwrap(); + while state.live_write_transaction.is_some() { + state = self.live_write_transaction_available.wait(state).unwrap(); + } + assert!(state.live_write_transaction.is_none()); + let transaction_id = state.next_transaction_id.increment(); + #[cfg(feature = "logging")] + debug!("Beginning write transaction id={transaction_id:?}"); + state.live_write_transaction = Some(transaction_id); + + transaction_id + } + + pub(crate) fn end_write_transaction(&self, id: TransactionId) { + let mut state = self.state.lock().unwrap(); + assert_eq!(state.live_write_transaction.unwrap(), id); + state.live_write_transaction = None; + self.live_write_transaction_available.notify_one(); + } + + pub(crate) fn clear_pending_non_durable_commits(&self) { + let mut state = self.state.lock().unwrap(); + let ids = mem::take(&mut state.pending_non_durable_commits); + for (_, durable_ancestor) in ids { + let ref_count = state + .live_read_transactions + .get_mut(&durable_ancestor) + .unwrap(); + *ref_count -= 1; + if *ref_count == 0 { + state.live_read_transactions.remove(&durable_ancestor); + } + } + } + + pub(crate) fn is_unprocessed_non_durable_commit(&self, id: TransactionId) -> bool { + let state = self.state.lock().unwrap(); + state.unprocessed_freed_non_durable_commits.contains(&id) + } + + pub(crate) fn mark_unprocessed_non_durable_commit(&self, id: TransactionId) { + let mut state = self.state.lock().unwrap(); + state.unprocessed_freed_non_durable_commits.remove(&id); + } + + pub(crate) fn oldest_unprocessed_non_durable_commit(&self) -> Option { + let state = self.state.lock().unwrap(); + state + .unprocessed_freed_non_durable_commits + .iter() + .next() + .copied() + } + + pub(crate) fn register_non_durable_commit( + &self, + id: TransactionId, + durable_ancestor: TransactionId, + ) { + let mut state = self.state.lock().unwrap(); + state + .live_read_transactions + .entry(durable_ancestor) + .and_modify(|x| *x += 1) + .or_insert(1); + state + .pending_non_durable_commits + .insert(id, durable_ancestor); + state.unprocessed_freed_non_durable_commits.insert(id); + } + + pub(crate) fn restore_savepoint_counter_state(&self, next_savepoint: SavepointId) { + let mut state = self.state.lock().unwrap(); + assert!(state.valid_savepoints.is_empty()); + state.next_savepoint_id = next_savepoint; + } + + pub(crate) fn register_persistent_savepoint(&self, savepoint: &Savepoint) { + let mut state = self.state.lock().unwrap(); + state + .live_read_transactions + .entry(savepoint.get_transaction_id()) + .and_modify(|x| *x += 1) + .or_insert(1); + state + .valid_savepoints + .insert(savepoint.get_id(), savepoint.get_transaction_id()); + } + + pub(crate) fn register_read_transaction( + &self, + mem: &TransactionalMemory, + ) -> Result { + let mut state = self.state.lock()?; + let id = mem.get_last_committed_transaction_id()?; + state + .live_read_transactions + .entry(id) + .and_modify(|x| *x += 1) + .or_insert(1); + + Ok(id) + } + + pub(crate) fn deallocate_read_transaction(&self, id: TransactionId) { + let mut state = self.state.lock().unwrap(); + let ref_count = state.live_read_transactions.get_mut(&id).unwrap(); + *ref_count -= 1; + if *ref_count == 0 { + state.live_read_transactions.remove(&id); + } + } + + pub(crate) fn any_savepoint_exists(&self) -> bool { + !self.state.lock().unwrap().valid_savepoints.is_empty() + } + + pub(crate) fn allocate_savepoint(&self, transaction_id: TransactionId) -> SavepointId { + let mut state = self.state.lock().unwrap(); + let id = state.next_savepoint_id.next(); + state.next_savepoint_id = id; + state.valid_savepoints.insert(id, transaction_id); + id + } + + // Deallocates the given savepoint and its matching reference count on the transcation + pub(crate) fn deallocate_savepoint(&self, savepoint: SavepointId, transaction: TransactionId) { + self.state + .lock() + .unwrap() + .valid_savepoints + .remove(&savepoint); + self.deallocate_read_transaction(transaction); + } + + pub(crate) fn is_valid_savepoint(&self, id: SavepointId) -> bool { + self.state + .lock() + .unwrap() + .valid_savepoints + .contains_key(&id) + } + + pub(crate) fn list_savepoints_after(&self, id: SavepointId) -> Vec { + self.state + .lock() + .unwrap() + .valid_savepoints + .range(( + std::ops::Bound::Excluded(id), + std::ops::Bound::Unbounded::, + )) + .map(|(x, _)| *x) + .collect() + } + + // Removes the given savepoints from the in-memory `valid_savepoints` map without touching + // live_read_transactions refs. The caller is responsible for making sure those refs are + // released by some other means: ephemeral `Savepoint::drop` or `deallocate_savepoint` + // (called via `delete_persistent_savepoint`) do that for their respective savepoint kinds. + // + // Savepoints that have already been removed (for example, by `deallocate_savepoint` earlier + // in the same transaction) are silently skipped. + pub(crate) fn invalidate_savepoints(&self, savepoints: impl IntoIterator) { + let mut state = self.state.lock().unwrap(); + for id in savepoints { + state.valid_savepoints.remove(&id); + } + } + + pub(crate) fn oldest_savepoint(&self) -> Option<(SavepointId, TransactionId)> { + self.state + .lock() + .unwrap() + .valid_savepoints + .first_key_value() + .map(|x| (*x.0, *x.1)) + } + + pub(crate) fn oldest_live_read_transaction(&self) -> Option { + self.state + .lock() + .unwrap() + .live_read_transactions + .keys() + .next() + .copied() + } + + // Returns the transaction id of the oldest non-durable transaction which has not been processed + // for freeing, which has live read transactions + pub(crate) fn oldest_live_read_nondurable_transaction(&self) -> Option { + let state = self.state.lock().unwrap(); + for id in state.live_read_transactions.keys() { + if state.pending_non_durable_commits.contains_key(id) { + return Some(*id); + } + } + None + } +} diff --git a/v2/redb/src/transactions.rs b/v2/redb/src/transactions.rs new file mode 100644 index 000000000..a303c869d --- /dev/null +++ b/v2/redb/src/transactions.rs @@ -0,0 +1,2287 @@ +use crate::db::TransactionGuard; +use crate::error::CommitError; +use crate::multimap_table::ReadOnlyUntypedMultimapTable; +use crate::sealed::Sealed; +use crate::table::ReadOnlyUntypedTable; +use crate::transaction_tracker::{SavepointId, TransactionId, TransactionTracker}; +use crate::tree_store::{ + Btree, BtreeHeader, BtreeMut, InternalTableDefinition, MAX_PAIR_LENGTH, MAX_VALUE_LENGTH, Page, + PageHint, PageListMut, PageNumber, PageTrackerPolicy, SerializedSavepoint, ShrinkPolicy, + TableTree, TableTreeMut, TableType, TransactionalMemory, +}; +use crate::types::{Key, Value}; +use crate::{ + AccessGuard, AccessGuardMutInPlace, ExtractIf, MultimapTable, MultimapTableDefinition, + MultimapTableHandle, MutInPlaceValue, Range, ReadOnlyMultimapTable, ReadOnlyTable, Result, + Savepoint, SavepointError, SetDurabilityError, StorageError, Table, TableDefinition, + TableError, TableHandle, TransactionError, TypeName, UntypedMultimapTableHandle, + UntypedTableHandle, +}; +#[cfg(feature = "logging")] +use log::{debug, warn}; +use std::borrow::Borrow; +use std::cmp::min; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::fmt::{Debug, Display, Formatter}; +use std::marker::PhantomData; +use std::mem::size_of; +use std::ops::RangeBounds; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::{panic, thread}; + +const MAX_PAGES_PER_COMPACTION: usize = 1_000_000; +const NEXT_SAVEPOINT_TABLE: SystemTableDefinition<(), SavepointId> = + SystemTableDefinition::new("next_savepoint_id"); +pub(crate) const SAVEPOINT_TABLE: SystemTableDefinition = + SystemTableDefinition::new("persistent_savepoints"); +// Pages that were allocated in the data tree by a given transaction. Only updated when a savepoint +// exists +pub(crate) const DATA_ALLOCATED_TABLE: SystemTableDefinition< + TransactionIdWithPagination, + PageList, +> = SystemTableDefinition::new("data_pages_allocated"); +// Pages in the data tree that are in the pending free state: i.e., they are unreachable from the +// root as of the given transaction. +pub(crate) const DATA_FREED_TABLE: SystemTableDefinition = + SystemTableDefinition::new("data_pages_unreachable"); +// Pages in the system tree that are in the pending free state: i.e., they are unreachable from the +// root as of the given transaction. +pub(crate) const SYSTEM_FREED_TABLE: SystemTableDefinition = + SystemTableDefinition::new("system_pages_unreachable"); +// The allocator state table is stored in the system table tree, but it's accessed using +// raw btree operations rather than open_system_table(), so there's no SystemTableDefinition +pub(crate) const ALLOCATOR_STATE_TABLE_NAME: &str = "allocator_state"; +pub(crate) type AllocatorStateTree = Btree; +pub(crate) type AllocatorStateTreeMut<'a> = BtreeMut<'a, AllocatorStateKey, &'static [u8]>; +pub(crate) type SystemFreedTree<'a> = BtreeMut<'a, TransactionIdWithPagination, PageList<'static>>; + +// Format: +// 2 bytes: length +// length * size_of(PageNumber): array of page numbers +#[derive(Debug)] +pub(crate) struct PageList<'a> { + data: &'a [u8], +} + +impl PageList<'_> { + fn required_bytes(len: usize) -> usize { + 2 + PageNumber::serialized_size() * len + } + + pub(crate) fn len(&self) -> usize { + u16::from_le_bytes(self.data[..size_of::()].try_into().unwrap()).into() + } + + pub(crate) fn get(&self, index: usize) -> PageNumber { + let start = size_of::() + PageNumber::serialized_size() * index; + PageNumber::from_le_bytes( + self.data[start..(start + PageNumber::serialized_size())] + .try_into() + .unwrap(), + ) + } +} + +impl Value for PageList<'_> { + type SelfType<'a> + = PageList<'a> + where + Self: 'a; + type AsBytes<'a> + = &'a [u8] + where + Self: 'a; + + fn fixed_width() -> Option { + None + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + PageList { data } + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> &'b [u8] + where + Self: 'b, + { + value.data + } + + fn type_name() -> TypeName { + TypeName::internal("redb::PageList") + } +} + +impl MutInPlaceValue for PageList<'_> { + type BaseRefType = PageListMut; + + fn initialize(data: &mut [u8]) { + assert!(data.len() >= 8); + // Set the length to zero + data[..8].fill(0); + } + + fn from_bytes_mut(data: &mut [u8]) -> &mut Self::BaseRefType { + unsafe { &mut *(std::ptr::from_mut::<[u8]>(data) as *mut PageListMut) } + } +} + +#[derive(Debug)] +pub(crate) struct TransactionIdWithPagination { + pub(crate) transaction_id: u64, + pub(crate) pagination_id: u64, +} + +impl Value for TransactionIdWithPagination { + type SelfType<'a> + = TransactionIdWithPagination + where + Self: 'a; + type AsBytes<'a> + = [u8; 2 * size_of::()] + where + Self: 'a; + + fn fixed_width() -> Option { + Some(2 * size_of::()) + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self + where + Self: 'a, + { + let transaction_id = u64::from_le_bytes(data[..size_of::()].try_into().unwrap()); + let pagination_id = u64::from_le_bytes(data[size_of::()..].try_into().unwrap()); + Self { + transaction_id, + pagination_id, + } + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> [u8; 2 * size_of::()] + where + Self: 'b, + { + let mut result = [0u8; 2 * size_of::()]; + result[..size_of::()].copy_from_slice(&value.transaction_id.to_le_bytes()); + result[size_of::()..].copy_from_slice(&value.pagination_id.to_le_bytes()); + result + } + + fn type_name() -> TypeName { + TypeName::internal("redb::TransactionIdWithPagination") + } +} + +impl Key for TransactionIdWithPagination { + fn compare(data1: &[u8], data2: &[u8]) -> std::cmp::Ordering { + let value1 = Self::from_bytes(data1); + let value2 = Self::from_bytes(data2); + + match value1.transaction_id.cmp(&value2.transaction_id) { + std::cmp::Ordering::Greater => std::cmp::Ordering::Greater, + std::cmp::Ordering::Equal => value1.pagination_id.cmp(&value2.pagination_id), + std::cmp::Ordering::Less => std::cmp::Ordering::Less, + } + } +} + +#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)] +pub(crate) enum AllocatorStateKey { + Deprecated, + Region(u32), + RegionTracker, + TransactionId, +} + +impl Value for AllocatorStateKey { + type SelfType<'a> = Self; + type AsBytes<'a> = [u8; 1 + size_of::()]; + + fn fixed_width() -> Option { + Some(1 + size_of::()) + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + match data[0] { + // 0, 1, 2 were used in redb 2.x and have a different format + 0..=2 => Self::Deprecated, + 3 => Self::Region(u32::from_le_bytes(data[1..].try_into().unwrap())), + 4 => Self::RegionTracker, + 5 => Self::TransactionId, + _ => unreachable!(), + } + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'a, + Self: 'b, + { + let mut result = Self::AsBytes::default(); + match value { + Self::Region(region) => { + result[0] = 3; + result[1..].copy_from_slice(&u32::to_le_bytes(*region)); + } + Self::RegionTracker => { + result[0] = 4; + } + Self::TransactionId => { + result[0] = 5; + } + AllocatorStateKey::Deprecated => { + result[0] = 0; + } + } + + result + } + + fn type_name() -> TypeName { + TypeName::internal("redb::AllocatorStateKey") + } +} + +impl Key for AllocatorStateKey { + fn compare(data1: &[u8], data2: &[u8]) -> std::cmp::Ordering { + Self::from_bytes(data1).cmp(&Self::from_bytes(data2)) + } +} + +pub struct SystemTableDefinition<'a, K: Key + 'static, V: Value + 'static> { + name: &'a str, + _key_type: PhantomData, + _value_type: PhantomData, +} + +impl<'a, K: Key + 'static, V: Value + 'static> SystemTableDefinition<'a, K, V> { + pub const fn new(name: &'a str) -> Self { + assert!(!name.is_empty()); + Self { + name, + _key_type: PhantomData, + _value_type: PhantomData, + } + } +} + +impl TableHandle for SystemTableDefinition<'_, K, V> { + fn name(&self) -> &str { + self.name + } +} + +impl Sealed for SystemTableDefinition<'_, K, V> {} + +impl Clone for SystemTableDefinition<'_, K, V> { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for SystemTableDefinition<'_, K, V> {} + +impl Display for SystemTableDefinition<'_, K, V> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}<{}, {}>", + self.name, + K::type_name().name(), + V::type_name().name() + ) + } +} + +/// Informational storage stats about the database +#[derive(Debug)] +pub struct DatabaseStats { + pub(crate) tree_height: u32, + pub(crate) allocated_pages: u64, + pub(crate) leaf_pages: u64, + pub(crate) branch_pages: u64, + pub(crate) stored_leaf_bytes: u64, + pub(crate) metadata_bytes: u64, + pub(crate) fragmented_bytes: u64, + pub(crate) page_size: usize, +} + +impl DatabaseStats { + /// Maximum traversal distance to reach the deepest (key, value) pair, across all tables + pub fn tree_height(&self) -> u32 { + self.tree_height + } + + /// Number of pages allocated + pub fn allocated_pages(&self) -> u64 { + self.allocated_pages + } + + /// Number of leaf pages that store user data + pub fn leaf_pages(&self) -> u64 { + self.leaf_pages + } + + /// Number of branch pages in btrees that store user data + pub fn branch_pages(&self) -> u64 { + self.branch_pages + } + + /// Number of bytes consumed by keys and values that have been inserted. + /// Does not include indexing overhead + pub fn stored_bytes(&self) -> u64 { + self.stored_leaf_bytes + } + + /// Number of bytes consumed by keys in internal branch pages, plus other metadata + pub fn metadata_bytes(&self) -> u64 { + self.metadata_bytes + } + + /// Number of bytes consumed by fragmentation, both in data pages and internal metadata tables + pub fn fragmented_bytes(&self) -> u64 { + self.fragmented_bytes + } + + /// Number of bytes per page + pub fn page_size(&self) -> usize { + self.page_size + } +} + +#[derive(Copy, Clone, Debug)] +#[non_exhaustive] +pub enum Durability { + /// Commits with this durability level will not be persisted to disk unless followed by a + /// commit with [`Durability::Immediate`]. + None, + /// Commits with this durability level are guaranteed to be persistent as soon as + /// [`WriteTransaction::commit`] returns. + Immediate, +} + +// These are the actual durability levels used internally. `Durability::Paranoid` is translated +// to `InternalDurability::Immediate`, and also enables 2-phase commit +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +enum InternalDurability { + None, + Immediate, +} + +// Like a Table but only one may be open at a time to avoid possible races +pub struct SystemTable<'db, 's, K: Key + 'static, V: Value + 'static> { + name: String, + namespace: &'s mut SystemNamespace<'db>, + tree: BtreeMut<'s, K, V>, + transaction_guard: Arc, +} + +impl<'db, 's, K: Key + 'static, V: Value + 'static> SystemTable<'db, 's, K, V> { + fn new( + name: &str, + table_root: Option, + freed_pages: Arc>>, + guard: Arc, + mem: Arc, + namespace: &'s mut SystemNamespace<'db>, + ) -> SystemTable<'db, 's, K, V> { + // No need to track allocations in the system tree. Savepoint restoration only relies on + // freeing in the data tree + let ignore = Arc::new(Mutex::new(PageTrackerPolicy::Ignore)); + SystemTable { + name: name.to_string(), + namespace, + tree: BtreeMut::new(table_root, guard.clone(), mem, freed_pages, ignore), + transaction_guard: guard, + } + } + + fn get<'a>(&self, key: impl Borrow>) -> Result>> + where + K: 'a, + { + self.tree.get(key.borrow()) + } + + fn range<'a, KR>(&self, range: impl RangeBounds + 'a) -> Result> + where + K: 'a, + KR: Borrow> + 'a, + { + self.tree + .range(&range) + .map(|x| Range::new(x, self.transaction_guard.clone())) + } + + pub fn extract_from_if<'a, KR, F: for<'f> FnMut(K::SelfType<'f>, V::SelfType<'f>) -> bool>( + &mut self, + range: impl RangeBounds + 'a, + predicate: F, + ) -> Result> + where + KR: Borrow> + 'a, + { + self.tree + .extract_from_if(&range, predicate) + .map(ExtractIf::new) + } + + pub fn insert<'k, 'v>( + &mut self, + key: impl Borrow>, + value: impl Borrow>, + ) -> Result>> { + let value_len = V::as_bytes(value.borrow()).as_ref().len(); + if value_len > MAX_VALUE_LENGTH { + return Err(StorageError::ValueTooLarge(value_len)); + } + let key_len = K::as_bytes(key.borrow()).as_ref().len(); + if key_len > MAX_VALUE_LENGTH { + return Err(StorageError::ValueTooLarge(key_len)); + } + if value_len + key_len > MAX_PAIR_LENGTH { + return Err(StorageError::ValueTooLarge(value_len + key_len)); + } + self.tree.insert(key.borrow(), value.borrow()) + } + + pub fn remove<'a>( + &mut self, + key: impl Borrow>, + ) -> Result>> + where + K: 'a, + { + self.tree.remove(key.borrow()) + } +} + +impl SystemTable<'_, '_, K, V> { + pub fn insert_reserve<'a>( + &mut self, + key: impl Borrow>, + value_length: usize, + ) -> Result> { + if value_length > MAX_VALUE_LENGTH { + return Err(StorageError::ValueTooLarge(value_length)); + } + let key_len = K::as_bytes(key.borrow()).as_ref().len(); + if key_len > MAX_VALUE_LENGTH { + return Err(StorageError::ValueTooLarge(key_len)); + } + if value_length + key_len > MAX_PAIR_LENGTH { + return Err(StorageError::ValueTooLarge(value_length + key_len)); + } + self.tree.insert_reserve(key.borrow(), value_length) + } +} + +impl Drop for SystemTable<'_, '_, K, V> { + fn drop(&mut self) { + self.namespace.close_table( + &self.name, + &self.tree, + self.tree.get_root().map(|x| x.length).unwrap_or_default(), + ); + } +} + +struct SystemNamespace<'db> { + table_tree: TableTreeMut<'db>, + freed_pages: Arc>>, + transaction_guard: Arc, +} + +impl<'db> SystemNamespace<'db> { + fn new( + root_page: Option, + guard: Arc, + mem: Arc, + ) -> Self { + // No need to track allocations in the system tree. Savepoint restoration only relies on + // freeing in the data tree + let ignore = Arc::new(Mutex::new(PageTrackerPolicy::Ignore)); + let freed_pages = Arc::new(Mutex::new(vec![])); + Self { + table_tree: TableTreeMut::new( + root_page, + guard.clone(), + mem, + freed_pages.clone(), + ignore, + ), + freed_pages, + transaction_guard: guard.clone(), + } + } + + fn system_freed_pages(&self) -> Arc>> { + self.freed_pages.clone() + } + + fn open_system_table<'txn, 's, K: Key + 'static, V: Value + 'static>( + &'s mut self, + transaction: &'txn WriteTransaction, + definition: SystemTableDefinition, + ) -> Result> { + let (root, _) = self + .table_tree + .get_or_create_table::(definition.name(), TableType::Normal) + .map_err(|e| { + e.into_storage_error_or_corrupted("Internal error. System table is corrupted") + })?; + transaction.dirty.store(true, Ordering::Release); + + Ok(SystemTable::new( + definition.name(), + root, + self.freed_pages.clone(), + self.transaction_guard.clone(), + transaction.mem.clone(), + self, + )) + } + + fn close_table( + &mut self, + name: &str, + table: &BtreeMut, + length: u64, + ) { + self.table_tree + .stage_update_table_root(name, table.get_root(), length); + } +} + +struct TableNamespace<'db> { + open_tables: HashMap>, + allocated_pages: Arc>, + freed_pages: Arc>>, + table_tree: TableTreeMut<'db>, +} + +impl TableNamespace<'_> { + fn new( + root_page: Option, + guard: Arc, + mem: Arc, + ) -> Self { + let allocated = Arc::new(Mutex::new(PageTrackerPolicy::new_tracking())); + let freed_pages = Arc::new(Mutex::new(vec![])); + let table_tree = TableTreeMut::new( + root_page, + guard, + mem, + // Committed pages which are no longer reachable and will be queued for free'ing + // These are separated from the system freed pages + freed_pages.clone(), + allocated.clone(), + ); + Self { + open_tables: Default::default(), + table_tree, + freed_pages, + allocated_pages: allocated, + } + } + + fn set_dirty(&mut self, transaction: &WriteTransaction) { + transaction.dirty.store(true, Ordering::Release); + if !transaction.transaction_tracker.any_savepoint_exists() { + // No savepoints exist, and we don't allow savepoints to be created in a dirty transaction + // so we can disable allocation tracking now + *self.allocated_pages.lock().unwrap() = PageTrackerPolicy::Ignore; + } + } + + fn set_root(&mut self, root: Option) { + assert!(self.open_tables.is_empty()); + self.table_tree.set_root(root); + } + + #[track_caller] + fn inner_open( + &mut self, + name: &str, + table_type: TableType, + ) -> Result<(Option, u64), TableError> { + if let Some(location) = self.open_tables.get(name) { + return Err(TableError::TableAlreadyOpen(name.to_string(), location)); + } + + let root = self + .table_tree + .get_or_create_table::(name, table_type)?; + self.open_tables + .insert(name.to_string(), panic::Location::caller()); + + Ok(root) + } + + #[track_caller] + pub fn open_multimap_table<'txn, K: Key + 'static, V: Key + 'static>( + &mut self, + transaction: &'txn WriteTransaction, + definition: MultimapTableDefinition, + ) -> Result, TableError> { + #[cfg(feature = "logging")] + debug!("Opening multimap table: {definition}"); + let (root, length) = self.inner_open::(definition.name(), TableType::Multimap)?; + self.set_dirty(transaction); + + Ok(MultimapTable::new( + definition.name(), + root, + length, + self.freed_pages.clone(), + self.allocated_pages.clone(), + transaction.mem.clone(), + transaction, + )) + } + + #[track_caller] + pub fn open_table<'txn, K: Key + 'static, V: Value + 'static>( + &mut self, + transaction: &'txn WriteTransaction, + definition: TableDefinition, + ) -> Result, TableError> { + #[cfg(feature = "logging")] + debug!("Opening table: {definition}"); + let (root, _) = self.inner_open::(definition.name(), TableType::Normal)?; + self.set_dirty(transaction); + + Ok(Table::new( + definition.name(), + root, + self.freed_pages.clone(), + self.allocated_pages.clone(), + transaction.mem.clone(), + transaction, + )) + } + + #[track_caller] + fn inner_rename( + &mut self, + name: &str, + new_name: &str, + table_type: TableType, + ) -> Result<(), TableError> { + if let Some(location) = self.open_tables.get(name) { + return Err(TableError::TableAlreadyOpen(name.to_string(), location)); + } + + self.table_tree.rename_table(name, new_name, table_type) + } + + #[track_caller] + fn rename_table( + &mut self, + transaction: &WriteTransaction, + name: &str, + new_name: &str, + ) -> Result<(), TableError> { + #[cfg(feature = "logging")] + debug!("Renaming table: {name} to {new_name}"); + self.set_dirty(transaction); + self.inner_rename(name, new_name, TableType::Normal) + } + + #[track_caller] + fn rename_multimap_table( + &mut self, + transaction: &WriteTransaction, + name: &str, + new_name: &str, + ) -> Result<(), TableError> { + #[cfg(feature = "logging")] + debug!("Renaming multimap table: {name} to {new_name}"); + self.set_dirty(transaction); + self.inner_rename(name, new_name, TableType::Multimap) + } + + #[track_caller] + fn inner_delete(&mut self, name: &str, table_type: TableType) -> Result { + if let Some(location) = self.open_tables.get(name) { + return Err(TableError::TableAlreadyOpen(name.to_string(), location)); + } + + self.table_tree.delete_table(name, table_type) + } + + #[track_caller] + fn delete_table( + &mut self, + transaction: &WriteTransaction, + name: &str, + ) -> Result { + #[cfg(feature = "logging")] + debug!("Deleting table: {name}"); + self.set_dirty(transaction); + self.inner_delete(name, TableType::Normal) + } + + #[track_caller] + fn delete_multimap_table( + &mut self, + transaction: &WriteTransaction, + name: &str, + ) -> Result { + #[cfg(feature = "logging")] + debug!("Deleting multimap table: {name}"); + self.set_dirty(transaction); + self.inner_delete(name, TableType::Multimap) + } + + pub(crate) fn close_table( + &mut self, + name: &str, + table: &BtreeMut, + length: u64, + ) { + self.open_tables.remove(name).unwrap(); + self.table_tree + .stage_update_table_root(name, table.get_root(), length); + } +} + +// Transaction-local savepoint lifecycle state. +#[derive(Default)] +struct SavepointTransactionState { + created_persistent: HashSet<(SavepointId, TransactionId)>, + deleted_persistent: Vec<(SavepointId, TransactionId)>, + invalidated: BTreeSet, +} + +impl SavepointTransactionState { + fn record_created(&mut self, id: SavepointId, transaction_id: TransactionId) { + self.created_persistent.insert((id, transaction_id)); + } + + fn record_deleted(&mut self, id: SavepointId, transaction_id: TransactionId) { + self.deleted_persistent.push((id, transaction_id)); + } + + fn record_invalidated(&mut self, ids: impl IntoIterator) { + self.invalidated.extend(ids); + } + + fn is_invalidated(&self, id: SavepointId) -> bool { + self.invalidated.contains(&id) + } + + fn has_created_or_deleted(&self) -> bool { + !self.created_persistent.is_empty() || !self.deleted_persistent.is_empty() + } + + fn apply_on_commit(&mut self, tracker: &TransactionTracker) { + // Persistent savepoints whose on-disk entry was deleted: release their + // tracker refcount now that the deletion is durable. + for (savepoint, transaction) in self.deleted_persistent.drain(..) { + tracker.deallocate_savepoint(savepoint, transaction); + } + // Savepoints that restore_savepoint() invalidated: remove them from the + // shared valid_savepoints map. For persistent savepoints, + // deallocate_savepoint above has already removed them; for ephemeral, + // the user's Savepoint handle still owns the live_read_transactions + // refcount and will release it on drop. + tracker.invalidate_savepoints(std::mem::take(&mut self.invalidated)); + // Persistent savepoints created during this transaction stay live: + // drop them from our bookkeeping without releasing tracker state. + self.created_persistent.clear(); + } + + fn apply_on_abort(&mut self, tracker: &TransactionTracker) { + // Persistent savepoints created during this transaction: their + // on-disk entries will be rolled back by rollback_uncommitted_writes(), + // but the shared tracker registration must be released explicitly. + for (savepoint, transaction) in self.created_persistent.drain() { + tracker.deallocate_savepoint(savepoint, transaction); + } + // Deleted-persistent entries will be rolled back on disk, so the + // tracker state must NOT be released (it is still valid). + self.deleted_persistent.clear(); + // Invalidations were only staged in this struct and never touched the + // shared tracker, so dropping them is sufficient. + self.invalidated.clear(); + } +} + +/// A read/write transaction +/// +/// Only a single [`WriteTransaction`] may exist at a time +pub struct WriteTransaction { + transaction_tracker: Arc, + mem: Arc, + transaction_guard: Arc, + transaction_id: TransactionId, + tables: Mutex>, + system_tables: Mutex>, + completed: bool, + dirty: AtomicBool, + durability: InternalDurability, + two_phase_commit: bool, + shrink_policy: ShrinkPolicy, + quick_repair: bool, + // All transaction-local savepoint lifecycle state. See + // `SavepointTransactionState` for the commit/abort contract. + savepoint_state: Mutex, +} + +impl WriteTransaction { + pub(crate) fn new( + guard: TransactionGuard, + transaction_tracker: Arc, + mem: Arc, + ) -> Result { + let transaction_id = guard.id(); + let guard = Arc::new(guard); + + let root_page = mem.get_data_root(); + let system_page = mem.get_system_root(); + + let tables = TableNamespace::new(root_page, guard.clone(), mem.clone()); + let system_tables = SystemNamespace::new(system_page, guard.clone(), mem.clone()); + + Ok(Self { + transaction_tracker, + mem: mem.clone(), + transaction_guard: guard.clone(), + transaction_id, + tables: Mutex::new(tables), + system_tables: Mutex::new(system_tables), + completed: false, + dirty: AtomicBool::new(false), + durability: InternalDurability::Immediate, + two_phase_commit: false, + quick_repair: false, + shrink_policy: ShrinkPolicy::Default, + savepoint_state: Mutex::new(SavepointTransactionState::default()), + }) + } + + pub(crate) fn set_shrink_policy(&mut self, shrink_policy: ShrinkPolicy) { + self.shrink_policy = shrink_policy; + } + + pub(crate) fn pending_free_pages(&self) -> Result { + let mut system_tables = self.system_tables.lock().unwrap(); + if system_tables + .open_system_table(self, DATA_FREED_TABLE)? + .tree + .get_root() + .is_some() + { + return Ok(true); + } + if system_tables + .open_system_table(self, SYSTEM_FREED_TABLE)? + .tree + .get_root() + .is_some() + { + return Ok(true); + } + + Ok(false) + } + + #[cfg(debug_assertions)] + pub fn print_allocated_page_debug(&self) { + let mut all_allocated: HashSet = + HashSet::from_iter(self.mem.all_allocated_pages()); + + self.mem.debug_check_allocator_consistency(); + + let mut table_pages = vec![]; + self.tables + .lock() + .unwrap() + .table_tree + .visit_all_pages(|path| { + table_pages.push(path.page_number()); + Ok(()) + }) + .unwrap(); + println!("Tables"); + for p in table_pages { + assert!(all_allocated.remove(&p)); + println!("{p:?}"); + } + + let mut system_table_pages = vec![]; + self.system_tables + .lock() + .unwrap() + .table_tree + .visit_all_pages(|path| { + system_table_pages.push(path.page_number()); + Ok(()) + }) + .unwrap(); + println!("System tables"); + for p in system_table_pages { + assert!(all_allocated.remove(&p)); + println!("{p:?}"); + } + + { + println!("Pending free (in data freed table)"); + let mut system_tables = self.system_tables.lock().unwrap(); + let data_freed = system_tables + .open_system_table(self, DATA_FREED_TABLE) + .unwrap(); + for entry in data_freed.range::(..).unwrap() { + let (_, entry) = entry.unwrap(); + let value = entry.value(); + for i in 0..value.len() { + let p = value.get(i); + assert!(all_allocated.remove(&p)); + println!("{p:?}"); + } + } + } + { + println!("Pending free (in system freed table)"); + let mut system_tables = self.system_tables.lock().unwrap(); + let system_freed = system_tables + .open_system_table(self, SYSTEM_FREED_TABLE) + .unwrap(); + for entry in system_freed + .range::(..) + .unwrap() + { + let (_, entry) = entry.unwrap(); + let value = entry.value(); + for i in 0..value.len() { + let p = value.get(i); + assert!(all_allocated.remove(&p)); + println!("{p:?}"); + } + } + } + { + let tables = self.tables.lock().unwrap(); + let pages = tables.freed_pages.lock().unwrap(); + if !pages.is_empty() { + println!("Pages in in-memory data freed_pages"); + for p in pages.iter() { + println!("{p:?}"); + assert!(all_allocated.remove(p)); + } + } + } + { + let system_tables = self.system_tables.lock().unwrap(); + let pages = system_tables.freed_pages.lock().unwrap(); + if !pages.is_empty() { + println!("Pages in in-memory system freed_pages"); + for p in pages.iter() { + println!("{p:?}"); + assert!(all_allocated.remove(p)); + } + } + } + if !all_allocated.is_empty() { + println!("Leaked pages"); + for p in all_allocated { + println!("{p:?}"); + } + } + } + + /// Creates a snapshot of the current database state, which can be used to rollback the database. + /// This savepoint will exist until it is deleted with `[delete_savepoint()]`. + /// + /// Note that while a savepoint exists, pages that become unused after it was created are not freed. + /// Therefore, the lifetime of a savepoint should be minimized. + /// + /// Returns `[SavepointError::InvalidSavepoint`], if the transaction is "dirty" (any tables have been opened), + /// or `[SavepointError::ImmediateDurabilityRequired]` if the transaction's durability is less than + /// `[Durability::Immediate]` + pub fn persistent_savepoint(&self) -> Result { + if self.durability != InternalDurability::Immediate { + return Err(SavepointError::ImmediateDurabilityRequired); + } + + let mut savepoint = self.ephemeral_savepoint()?; + + let mut system_tables = self.system_tables.lock().unwrap(); + + let mut next_table = system_tables.open_system_table(self, NEXT_SAVEPOINT_TABLE)?; + next_table.insert((), savepoint.get_id().next())?; + drop(next_table); + + let mut savepoint_table = system_tables.open_system_table(self, SAVEPOINT_TABLE)?; + savepoint_table.insert( + savepoint.get_id(), + SerializedSavepoint::from_savepoint(&savepoint), + )?; + + savepoint.set_persistent(); + + self.savepoint_state + .lock() + .unwrap() + .record_created(savepoint.get_id(), savepoint.get_transaction_id()); + + Ok(savepoint.get_id().0) + } + + pub(crate) fn transaction_guard(&self) -> Arc { + self.transaction_guard.clone() + } + + pub(crate) fn next_persistent_savepoint_id(&self) -> Result> { + let mut system_tables = self.system_tables.lock().unwrap(); + let next_table = system_tables.open_system_table(self, NEXT_SAVEPOINT_TABLE)?; + let value = next_table.get(())?; + if let Some(next_id) = value { + Ok(Some(next_id.value())) + } else { + Ok(None) + } + } + + /// Get a persistent savepoint given its id + pub fn get_persistent_savepoint(&self, id: u64) -> Result { + let mut system_tables = self.system_tables.lock().unwrap(); + let table = system_tables.open_system_table(self, SAVEPOINT_TABLE)?; + let value = table.get(SavepointId(id))?; + + value + .map(|x| x.value().to_savepoint(self.transaction_tracker.clone())) + .ok_or(SavepointError::InvalidSavepoint) + } + + /// Delete the given persistent savepoint. + /// + /// Note that if the transaction is `abort()`'ed this deletion will be rolled back. + /// + /// Returns `true` if the savepoint existed + /// Returns `[SavepointError::ImmediateDurabilityRequired]` if the transaction's durability + /// is less than `[Durability::Immediate]` + pub fn delete_persistent_savepoint(&self, id: u64) -> Result { + if self.durability != InternalDurability::Immediate { + return Err(SavepointError::ImmediateDurabilityRequired); + } + let mut system_tables = self.system_tables.lock().unwrap(); + let mut table = system_tables.open_system_table(self, SAVEPOINT_TABLE)?; + let savepoint = table.remove(SavepointId(id))?; + if let Some(serialized) = savepoint { + let savepoint = serialized + .value() + .to_savepoint(self.transaction_tracker.clone()); + self.savepoint_state + .lock() + .unwrap() + .record_deleted(savepoint.get_id(), savepoint.get_transaction_id()); + Ok(true) + } else { + Ok(false) + } + } + + /// List all persistent savepoints + pub fn list_persistent_savepoints(&self) -> Result> { + let mut system_tables = self.system_tables.lock().unwrap(); + let table = system_tables.open_system_table(self, SAVEPOINT_TABLE)?; + let mut savepoints = vec![]; + for savepoint in table.range::(..)? { + savepoints.push(savepoint?.0.value().0); + } + Ok(savepoints.into_iter()) + } + + // TODO: deduplicate this with the one in Database + fn allocate_read_transaction(&self) -> Result { + let id = self + .transaction_tracker + .register_read_transaction(&self.mem)?; + + Ok(TransactionGuard::new_read( + id, + self.transaction_tracker.clone(), + )) + } + + fn allocate_savepoint(&self) -> Result<(SavepointId, TransactionId)> { + let transaction_id = self.allocate_read_transaction()?.leak(); + let id = self.transaction_tracker.allocate_savepoint(transaction_id); + Ok((id, transaction_id)) + } + + /// Creates a snapshot of the current database state, which can be used to rollback the database + /// + /// This savepoint will be freed as soon as the returned `[Savepoint]` is dropped. + /// + /// Returns `[SavepointError::InvalidSavepoint`], if the transaction is "dirty" (any tables have been opened) + pub fn ephemeral_savepoint(&self) -> Result { + if self.dirty.load(Ordering::Acquire) { + return Err(SavepointError::InvalidSavepoint); + } + + let (id, transaction_id) = self.allocate_savepoint()?; + #[cfg(feature = "logging")] + debug!("Creating savepoint id={id:?}, txn_id={transaction_id:?}"); + + let root = self.mem.get_data_root(); + let savepoint = Savepoint::new_ephemeral( + &self.mem, + self.transaction_tracker.clone(), + id, + transaction_id, + root, + ); + + Ok(savepoint) + } + + /// Restore the state of the database to the given [`Savepoint`] + /// + /// Calling this method invalidates all [`Savepoint`]s created after savepoint + pub fn restore_savepoint(&mut self, savepoint: &Savepoint) -> Result<(), SavepointError> { + // Reject a Savepoint that is from a different Database + if std::ptr::from_ref(self.transaction_tracker.as_ref()) != savepoint.db_address() { + return Err(SavepointError::InvalidSavepoint); + } + + if !self + .transaction_tracker + .is_valid_savepoint(savepoint.get_id()) + || self + .savepoint_state + .lock() + .unwrap() + .is_invalidated(savepoint.get_id()) + { + return Err(SavepointError::InvalidSavepoint); + } + + if self.durability != InternalDurability::Immediate + && self + .list_persistent_savepoints()? + .any(|id| id > savepoint.get_id().0) + { + return Err(SavepointError::ImmediateDurabilityRequired); + } + #[cfg(feature = "logging")] + debug!( + "Beginning savepoint restore (id={:?}) in transaction id={:?}", + savepoint.get_id(), + self.transaction_id + ); + // Restoring a savepoint that reverted a file format or checksum type change could corrupt + // the database + assert_eq!(self.mem.get_version(), savepoint.get_version()); + self.dirty.store(true, Ordering::Release); + + // Restoring a savepoint needs to accomplish the following: + // 1) restore the table tree. This is trivial, since we have the old root + // 1a) we also filter the freed tree to remove any pages referenced by the old root + // 2) free all pages that were allocated since the savepoint and are unreachable + // from the restored table tree root. Here we diff the reachable pages from the old + // and new roots + // 3) update the system tree to remove invalid persistent savepoints. + + // 1) restore the table tree + { + self.tables + .lock() + .unwrap() + .set_root(savepoint.get_user_root()); + } + + // 1a) purge all transactions that happened after the savepoint from the data freed tree + let txn_id = savepoint.get_transaction_id().next().raw_id(); + { + let lower = TransactionIdWithPagination { + transaction_id: txn_id, + pagination_id: 0, + }; + let mut system_tables = self.system_tables.lock().unwrap(); + let mut data_freed = system_tables.open_system_table(self, DATA_FREED_TABLE)?; + for entry in data_freed.extract_from_if(lower.., |_, _| true)? { + entry?; + } + // No need to process the system freed table, because it only rolls forward + } + + // 2) queue all pages that became unreachable + { + let tables = self.tables.lock().unwrap(); + for page in tables.allocated_pages.lock().unwrap().reset() { + debug_assert!(self.mem.uncommitted(page)); + debug_assert!(self.mem.is_allocated(page)); + self.mem.free(page, &mut PageTrackerPolicy::Ignore); + } + let mut data_freed_pages = tables.freed_pages.lock().unwrap(); + data_freed_pages.clear(); + let mut system_tables = self.system_tables.lock().unwrap(); + let data_allocated = system_tables.open_system_table(self, DATA_ALLOCATED_TABLE)?; + let lower = TransactionIdWithPagination { + transaction_id: txn_id, + pagination_id: 0, + }; + for entry in data_allocated.range(lower..)? { + let (_, value) = entry?; + for i in 0..value.value().len() { + data_freed_pages.push(value.value().get(i)); + } + } + } + + // 3) Mark all savepoints newer than the restored one as invalidated for this + // transaction, to prevent the user from later trying to restore a savepoint + // "on another timeline". The invalidation is purely per-transaction state - + // the shared `valid_savepoints` map is only updated if/when commit_inner() + // runs, so an abort implicitly reverts the invalidation by dropping this set. + let invalidated = self + .transaction_tracker + .list_savepoints_after(savepoint.get_id()); + self.savepoint_state + .lock() + .unwrap() + .record_invalidated(invalidated); + for persistent_savepoint in self.list_persistent_savepoints()? { + if persistent_savepoint > savepoint.get_id().0 { + self.delete_persistent_savepoint(persistent_savepoint)?; + } + } + + Ok(()) + } + + /// Set the desired durability level for writes made in this transaction + /// Defaults to [`Durability::Immediate`] + /// + /// If a persistent savepoint has been created or deleted, in this transaction, the durability may not + /// be reduced below [`Durability::Immediate`] + pub fn set_durability(&mut self, durability: Durability) -> Result<(), SetDurabilityError> { + let persistent_modified = self + .savepoint_state + .lock() + .unwrap() + .has_created_or_deleted(); + if persistent_modified && !matches!(durability, Durability::Immediate) { + return Err(SetDurabilityError::PersistentSavepointModified); + } + + self.durability = match durability { + Durability::None => InternalDurability::None, + Durability::Immediate => InternalDurability::Immediate, + }; + + Ok(()) + } + + /// Enable or disable 2-phase commit (defaults to disabled) + /// + /// By default, data is written using the following 1-phase commit algorithm: + /// + /// 1. Update the inactive commit slot with the new database state + /// 2. Flip the god byte primary bit to activate the newly updated commit slot + /// 3. Call `fsync` to ensure all writes have been persisted to disk + /// + /// All data is written with checksums. When opening the database after a crash, the most + /// recent of the two commit slots with a valid checksum is used. + /// + /// Security considerations: The checksum used is xxhash, a fast, non-cryptographic hash + /// function with close to perfect collision resistance when used with non-malicious input. An + /// attacker with an extremely high degree of control over the database's workload, including + /// the ability to cause the database process to crash, can cause invalid data to be written + /// with a valid checksum, leaving the database in an invalid, attacker-controlled state. + /// + /// Alternatively, you can enable 2-phase commit, which writes data like this: + /// + /// 1. Update the inactive commit slot with the new database state + /// 2. Call `fsync` to ensure the database slate and commit slot update have been persisted + /// 3. Flip the god byte primary bit to activate the newly updated commit slot + /// 4. Call `fsync` to ensure the write to the god byte has been persisted + /// + /// This mitigates a theoretical attack where an attacker who + /// 1. can control the order in which pages are flushed to disk + /// 2. can introduce crashes during `fsync`, + /// 3. has knowledge of the database file contents, and + /// 4. can include arbitrary data in a write transaction + /// + /// could cause a transaction to partially commit (some but not all of the data is written). + /// This is described in the design doc in futher detail. + /// + /// Security considerations: Many hard disk drives and SSDs do not actually guarantee that data + /// has been persisted to disk after calling `fsync`. Even with 2-phase commit, an attacker with + /// a high degree of control over the database's workload, including the ability to cause the + /// database process to crash, can cause the database to crash with the god byte primary bit + /// pointing to an invalid commit slot, leaving the database in an invalid, potentially attacker- + /// controlled state. + pub fn set_two_phase_commit(&mut self, enabled: bool) { + self.two_phase_commit = enabled; + } + + /// Enable or disable quick-repair (defaults to disabled) + /// + /// By default, when reopening the database after a crash, redb needs to do a full repair. + /// This involves walking the entire database to verify the checksums and reconstruct the + /// allocator state, so it can be very slow if the database is large. + /// + /// Alternatively, you can enable quick-repair. In this mode, redb saves the allocator state + /// as part of each commit (so it doesn't need to be reconstructed), and enables 2-phase commit + /// (which guarantees that the primary commit slot is valid without needing to look at the + /// checksums). This means commits are slower, but recovery after a crash is almost instant. + pub fn set_quick_repair(&mut self, enabled: bool) { + self.quick_repair = enabled; + } + + /// Open the given table + /// + /// The table will be created if it does not exist + #[track_caller] + pub fn open_table<'txn, K: Key + 'static, V: Value + 'static>( + &'txn self, + definition: TableDefinition, + ) -> Result, TableError> { + self.tables.lock().unwrap().open_table(self, definition) + } + + /// Open the given table + /// + /// The table will be created if it does not exist + #[track_caller] + pub fn open_multimap_table<'txn, K: Key + 'static, V: Key + 'static>( + &'txn self, + definition: MultimapTableDefinition, + ) -> Result, TableError> { + self.tables + .lock() + .unwrap() + .open_multimap_table(self, definition) + } + + pub(crate) fn close_table( + &self, + name: &str, + table: &BtreeMut, + length: u64, + ) { + self.tables.lock().unwrap().close_table(name, table, length); + } + + /// Rename the given table + pub fn rename_table( + &self, + definition: impl TableHandle, + new_name: impl TableHandle, + ) -> Result<(), TableError> { + let name = definition.name().to_string(); + // Drop the definition so that callers can pass in a `Table` to rename, without getting a TableAlreadyOpen error + drop(definition); + self.tables + .lock() + .unwrap() + .rename_table(self, &name, new_name.name()) + } + + /// Rename the given multimap table + pub fn rename_multimap_table( + &self, + definition: impl MultimapTableHandle, + new_name: impl MultimapTableHandle, + ) -> Result<(), TableError> { + let name = definition.name().to_string(); + // Drop the definition so that callers can pass in a `MultimapTable` to rename, without getting a TableAlreadyOpen error + drop(definition); + self.tables + .lock() + .unwrap() + .rename_multimap_table(self, &name, new_name.name()) + } + + /// Delete the given table + /// + /// Returns a bool indicating whether the table existed + pub fn delete_table(&self, definition: impl TableHandle) -> Result { + let name = definition.name().to_string(); + // Drop the definition so that callers can pass in a `Table` or `MultimapTable` to delete, without getting a TableAlreadyOpen error + drop(definition); + self.tables.lock().unwrap().delete_table(self, &name) + } + + /// Delete the given table + /// + /// Returns a bool indicating whether the table existed + pub fn delete_multimap_table( + &self, + definition: impl MultimapTableHandle, + ) -> Result { + let name = definition.name().to_string(); + // Drop the definition so that callers can pass in a `Table` or `MultimapTable` to delete, without getting a TableAlreadyOpen error + drop(definition); + self.tables + .lock() + .unwrap() + .delete_multimap_table(self, &name) + } + + /// List all the tables + pub fn list_tables(&self) -> Result + '_> { + self.tables + .lock() + .unwrap() + .table_tree + .list_tables(TableType::Normal) + .map(|x| x.into_iter().map(UntypedTableHandle::new)) + } + + /// List all the multimap tables + pub fn list_multimap_tables( + &self, + ) -> Result + '_> { + self.tables + .lock() + .unwrap() + .table_tree + .list_tables(TableType::Multimap) + .map(|x| x.into_iter().map(UntypedMultimapTableHandle::new)) + } + + /// Commit the transaction + /// + /// All writes performed in this transaction will be visible to future transactions, and are + /// durable as consistent with the [`Durability`] level set by [`Self::set_durability`] + pub fn commit(mut self) -> Result<(), CommitError> { + // Set completed flag first, so that we don't go through the abort() path on drop, if this fails + self.completed = true; + self.commit_inner() + } + + fn commit_inner(&mut self) -> Result<(), CommitError> { + // Quick-repair requires 2-phase commit + if self.quick_repair { + self.two_phase_commit = true; + } + + let (user_root, allocated_pages, data_freed) = + self.tables.lock().unwrap().table_tree.flush_and_close()?; + + self.store_data_freed_pages(data_freed)?; + self.store_allocated_pages(allocated_pages.into_iter().collect())?; + + #[cfg(feature = "logging")] + debug!( + "Committing transaction id={:?} with durability={:?} two_phase={} quick_repair={}", + self.transaction_id, self.durability, self.two_phase_commit, self.quick_repair + ); + match self.durability { + InternalDurability::None => self.non_durable_commit(user_root)?, + InternalDurability::Immediate => self.durable_commit(user_root)?, + } + + self.savepoint_state + .lock() + .unwrap() + .apply_on_commit(&self.transaction_tracker); + + assert!( + self.system_tables + .lock() + .unwrap() + .system_freed_pages() + .lock() + .unwrap() + .is_empty() + ); + assert!( + self.tables + .lock() + .unwrap() + .freed_pages + .lock() + .unwrap() + .is_empty() + ); + + #[cfg(feature = "logging")] + debug!( + "Finished commit of transaction id={:?}", + self.transaction_id + ); + + Ok(()) + } + + fn store_data_freed_pages(&self, mut freed_pages: Vec) -> Result { + let mut system_tables = self.system_tables.lock().unwrap(); + let mut freed_table = system_tables.open_system_table(self, DATA_FREED_TABLE)?; + let mut pagination_counter = 0; + while !freed_pages.is_empty() { + let chunk_size = 400; + let buffer_size = PageList::required_bytes(chunk_size); + let key = TransactionIdWithPagination { + transaction_id: self.transaction_id.raw_id(), + pagination_id: pagination_counter, + }; + let mut access_guard = freed_table.insert_reserve(&key, buffer_size)?; + + let len = freed_pages.len(); + access_guard.as_mut().clear(); + for page in freed_pages.drain(len - min(len, chunk_size)..) { + // Make sure that the page is currently allocated + debug_assert!( + self.mem.is_allocated(page), + "Page is not allocated: {page:?}" + ); + debug_assert!(!self.mem.uncommitted(page), "Page is uncommitted: {page:?}"); + access_guard.as_mut().push_back(page); + } + + pagination_counter += 1; + } + + Ok(()) + } + + fn store_allocated_pages(&self, mut data_allocated_pages: Vec) -> Result { + let mut system_tables = self.system_tables.lock().unwrap(); + let mut allocated_table = system_tables.open_system_table(self, DATA_ALLOCATED_TABLE)?; + let mut pagination_counter = 0; + while !data_allocated_pages.is_empty() { + let chunk_size = 400; + let buffer_size = PageList::required_bytes(chunk_size); + let key = TransactionIdWithPagination { + transaction_id: self.transaction_id.raw_id(), + pagination_id: pagination_counter, + }; + let mut access_guard = allocated_table.insert_reserve(&key, buffer_size)?; + + let len = data_allocated_pages.len(); + access_guard.as_mut().clear(); + for page in data_allocated_pages.drain(len - min(len, chunk_size)..) { + // Make sure that the page is currently allocated. This is to catch scenarios like + // a page getting allocated, and then deallocated within the same transaction, + // but errantly being left in the allocated pages list + debug_assert!( + self.mem.is_allocated(page), + "Page is not allocated: {page:?}" + ); + debug_assert!(self.mem.uncommitted(page), "Page is committed: {page:?}"); + access_guard.as_mut().push_back(page); + } + + pagination_counter += 1; + } + + // Purge any transactions that are no longer referenced + let oldest = self + .transaction_tracker + .oldest_savepoint() + .map_or(u64::MAX, |(_, x)| x.raw_id()); + let key = TransactionIdWithPagination { + transaction_id: oldest, + pagination_id: 0, + }; + for entry in allocated_table.extract_from_if(..key, |_, _| true)? { + entry?; + } + + Ok(()) + } + + /// Abort the transaction + /// + /// All writes performed in this transaction will be rolled back + pub fn abort(mut self) -> Result { + // Set completed flag first, so that we don't go through the abort() path on drop, if this fails + self.completed = true; + self.abort_inner() + } + + fn abort_inner(&mut self) -> Result { + #[cfg(feature = "logging")] + debug!("Aborting transaction id={:?}", self.transaction_id); + self.tables + .lock() + .unwrap() + .table_tree + .clear_root_updates_and_close(); + // Release all transaction-local savepoint state. The on-disk mutations + // (SAVEPOINT_TABLE / NEXT_SAVEPOINT_TABLE) are reverted by + // rollback_uncommitted_writes() below; apply_on_abort handles the + // in-memory TransactionTracker state that rollback cannot see. + self.savepoint_state + .lock() + .unwrap() + .apply_on_abort(&self.transaction_tracker); + self.mem.rollback_uncommitted_writes()?; + #[cfg(feature = "logging")] + debug!("Finished abort of transaction id={:?}", self.transaction_id); + Ok(()) + } + + pub(crate) fn durable_commit(&mut self, user_root: Option) -> Result { + let free_until_transaction = self + .transaction_tracker + .oldest_live_read_transaction() + .map_or(self.transaction_id, |x| x.next()); + self.process_freed_pages(free_until_transaction)?; + + let mut system_tables = self.system_tables.lock().unwrap(); + let system_freed_pages = system_tables.system_freed_pages(); + let system_tree = system_tables.table_tree.flush_table_root_updates()?; + system_tree + .delete_table(ALLOCATOR_STATE_TABLE_NAME, TableType::Normal) + .map_err(|e| e.into_storage_error_or_corrupted("Unexpected TableError"))?; + + if self.quick_repair { + system_tree.create_table_and_flush_table_root( + ALLOCATOR_STATE_TABLE_NAME, + |system_tree_ref, tree: &mut AllocatorStateTreeMut| { + let mut pagination_counter = 0; + + loop { + let num_regions = self + .mem + .reserve_allocator_state(tree, self.transaction_id)?; + + // We can't free pages after the commit, because that would invalidate our + // saved allocator state. Everything needs to go through the transactional + // free mechanism + self.store_system_freed_pages( + system_tree_ref, + system_freed_pages.clone(), + None, + &mut pagination_counter, + )?; + + if self.mem.try_save_allocator_state(tree, num_regions)? { + return Ok(()); + } + + // Clear out the table before retrying, just in case the number of regions + // has somehow shrunk. Don't use retain_in() for this, since it doesn't + // free the pages immediately -- we need to reuse those pages to guarantee + // that our retry loop will eventually terminate + while let Some(guards) = tree.last()? { + let key = guards.0.value(); + drop(guards); + tree.remove(&key)?; + } + } + }, + )?; + } + + let system_root = system_tree.finalize_dirty_checksums()?; + + self.mem.commit( + user_root, + system_root, + self.transaction_id, + self.two_phase_commit, + self.shrink_policy, + )?; + + // Mark any pending non-durable commits as fully committed. + self.transaction_tracker.clear_pending_non_durable_commits(); + + // Immediately free the pages that were freed from the system-tree. These are only + // accessed by write transactions, so it's safe to free them as soon as the commit is done. + for page in system_freed_pages.lock().unwrap().drain(..) { + self.mem.free(page, &mut PageTrackerPolicy::Ignore); + } + + Ok(()) + } + + // Commit without a durability guarantee + pub(crate) fn non_durable_commit(&mut self, user_root: Option) -> Result { + let mut free_until_transaction = self + .transaction_tracker + .oldest_live_read_nondurable_transaction() + .map_or(self.transaction_id, |x| x.next()); + // TODO: refactor the non-durable free'ed processing to remove this + // The reason it is needed is that non-durable commits edit previous non-durable commits, + // but they only edit the freed tree of unpersisted pages. + // The allocated tree, which savepoints rely, is not edited for performance reasons + // Therefore, we must not edit anything after a savepoint + // It would be better for non-durable transaction's unpersisted pages to be kept in-memory + // in a data structure where the allocated list can be efficiently edited + if let Some((_, oldest_savepoint)) = self.transaction_tracker.oldest_savepoint() { + free_until_transaction = TransactionId::min(free_until_transaction, oldest_savepoint); + } + self.process_freed_pages_nondurable(free_until_transaction)?; + + let mut post_commit_frees = vec![]; + + let system_root = { + let mut system_tables = self.system_tables.lock().unwrap(); + let system_freed_pages = system_tables.system_freed_pages(); + system_tables.table_tree.flush_table_root_updates()?; + for page in system_freed_pages + .lock() + .unwrap() + .extract_if(.., |p| self.mem.unpersisted(*p)) + { + post_commit_frees.push(page); + } + // Store all freed pages for a future commit(), since we can't free pages during a + // non-durable commit (it's non-durable, so could be rolled back anytime in the future) + self.store_system_freed_pages( + &mut system_tables.table_tree, + system_freed_pages, + Some(&mut post_commit_frees), + &mut 0, + )?; + + system_tables + .table_tree + .flush_table_root_updates()? + .finalize_dirty_checksums()? + }; + + self.mem + .non_durable_commit(user_root, system_root, self.transaction_id)?; + // Register this as a non-durable transaction to ensure that the freed pages we just pushed + // are only processed after this has been persisted + self.transaction_tracker.register_non_durable_commit( + self.transaction_id, + self.mem.get_last_durable_transaction_id()?, + ); + + for page in post_commit_frees { + self.mem.free(page, &mut PageTrackerPolicy::Ignore); + } + + Ok(()) + } + + // Relocate pages to lower number regions/pages + // Returns true if a page(s) was moved + pub(crate) fn compact_pages(&mut self) -> Result { + let mut progress = false; + + // Find the 1M highest pages + let mut highest_pages = BTreeMap::new(); + let mut tables = self.tables.lock().unwrap(); + let table_tree = &mut tables.table_tree; + table_tree.highest_index_pages(MAX_PAGES_PER_COMPACTION, &mut highest_pages)?; + let mut system_tables = self.system_tables.lock().unwrap(); + let system_table_tree = &mut system_tables.table_tree; + system_table_tree.highest_index_pages(MAX_PAGES_PER_COMPACTION, &mut highest_pages)?; + + // Calculate how many of them can be relocated to lower pages, starting from the last page + let mut relocation_map = HashMap::new(); + for path in highest_pages.into_values().rev() { + if relocation_map.contains_key(&path.page_number()) { + continue; + } + let old_page = self.mem.get_page(path.page_number(), PageHint::None)?; + let mut new_page = self.mem.allocate_lowest(old_page.memory().len())?; + let new_page_number = new_page.get_page_number(); + // We have to copy at least the page type into the new page. + // Otherwise its cache priority will be calculated incorrectly + new_page.memory_mut()[0] = old_page.memory()[0]; + drop(new_page); + // We're able to move this to a lower page, so insert it and rewrite all its parents + if new_page_number < path.page_number() { + relocation_map.insert(path.page_number(), new_page_number); + for parent in path.parents() { + if relocation_map.contains_key(parent) { + continue; + } + let old_parent = self.mem.get_page(*parent, PageHint::None)?; + let mut new_page = self.mem.allocate_lowest(old_parent.memory().len())?; + let new_page_number = new_page.get_page_number(); + // We have to copy at least the page type into the new page. + // Otherwise its cache priority will be calculated incorrectly + new_page.memory_mut()[0] = old_parent.memory()[0]; + drop(new_page); + relocation_map.insert(*parent, new_page_number); + } + } else { + self.mem + .free(new_page_number, &mut PageTrackerPolicy::Ignore); + break; + } + } + + if !relocation_map.is_empty() { + progress = true; + } + + table_tree.relocate_tables(&relocation_map)?; + system_table_tree.relocate_tables(&relocation_map)?; + + Ok(progress) + } + + // NOTE: must be called before store_system_freed_pages() during commit, since this can create + // more pages freed by the current transaction + fn process_freed_pages(&mut self, free_until: TransactionId) -> Result { + // We assume below that PageNumber is length 8 + assert_eq!(PageNumber::serialized_size(), 8); + + // Handle the data freed tree + let mut system_tables = self.system_tables.lock().unwrap(); + { + let mut data_freed = system_tables.open_system_table(self, DATA_FREED_TABLE)?; + let key = TransactionIdWithPagination { + transaction_id: free_until.raw_id(), + pagination_id: 0, + }; + for entry in data_freed.extract_from_if(..key, |_, _| true)? { + let (_, page_list) = entry?; + for i in 0..page_list.value().len() { + self.mem + .free(page_list.value().get(i), &mut PageTrackerPolicy::Ignore); + } + } + } + + // Handle the system freed tree + { + let mut system_freed = system_tables.open_system_table(self, SYSTEM_FREED_TABLE)?; + let key = TransactionIdWithPagination { + transaction_id: free_until.raw_id(), + pagination_id: 0, + }; + for entry in system_freed.extract_from_if(..key, |_, _| true)? { + let (_, page_list) = entry?; + for i in 0..page_list.value().len() { + self.mem + .free(page_list.value().get(i), &mut PageTrackerPolicy::Ignore); + } + } + } + + Ok(()) + } + + fn process_freed_pages_nondurable_helper( + &mut self, + free_until: TransactionId, + definition: SystemTableDefinition, + ) -> Result> { + let mut processed = vec![]; + let mut system_tables = self.system_tables.lock().unwrap(); + + let last_key = TransactionIdWithPagination { + transaction_id: free_until.raw_id(), + pagination_id: 0, + }; + let oldest_unprocessed = self + .transaction_tracker + .oldest_unprocessed_non_durable_commit() + .map_or(free_until.raw_id(), |x| x.raw_id()); + let first_key = TransactionIdWithPagination { + transaction_id: oldest_unprocessed, + pagination_id: 0, + }; + let mut data_freed = system_tables.open_system_table(self, definition)?; + + let mut candidate_transactions = vec![]; + for entry in data_freed.range(first_key..last_key)? { + let (key, _) = entry?; + let transaction_id = TransactionId::new(key.value().transaction_id); + if self + .transaction_tracker + .is_unprocessed_non_durable_commit(transaction_id) + { + candidate_transactions.push(transaction_id); + } + } + for transaction_id in candidate_transactions { + let mut key = TransactionIdWithPagination { + transaction_id: transaction_id.raw_id(), + pagination_id: 0, + }; + loop { + let Some(entry) = data_freed.get(&key)? else { + break; + }; + let pages = entry.value(); + let mut new_pages = vec![]; + for i in 0..pages.len() { + let page = pages.get(i); + if !self + .mem + .free_if_unpersisted(page, &mut PageTrackerPolicy::Ignore) + { + new_pages.push(page); + } + } + if new_pages.len() != pages.len() { + drop(entry); + if new_pages.is_empty() { + data_freed.remove(&key)?; + } else { + let required = PageList::required_bytes(new_pages.len()); + let mut page_list_mut = data_freed.insert_reserve(&key, required)?; + for page in new_pages { + page_list_mut.as_mut().push_back(page); + } + } + } + key.pagination_id += 1; + } + processed.push(transaction_id); + } + + Ok(processed) + } + + // NOTE: must be called before store_system_freed_pages() during commit, since this can create + // more pages freed by the current transaction + // + // This method only frees pages that are unpersisted, in non-durable transactions, since + // it is called from a non-durable commit() and therefore can't modify anything that the + // on-disk state in the last durable transaction might reference. + fn process_freed_pages_nondurable(&mut self, free_until: TransactionId) -> Result { + // We assume below that PageNumber is length 8 + assert_eq!(PageNumber::serialized_size(), 8); + + // Handle the data freed tree + let mut processed = + self.process_freed_pages_nondurable_helper(free_until, DATA_FREED_TABLE)?; + + // Handle the system freed tree + processed + .extend(self.process_freed_pages_nondurable_helper(free_until, SYSTEM_FREED_TABLE)?); + + for transaction_id in processed { + self.transaction_tracker + .mark_unprocessed_non_durable_commit(transaction_id); + } + + Ok(()) + } + + fn store_system_freed_pages( + &self, + system_tree: &mut TableTreeMut, + system_freed_pages: Arc>>, + mut unpersisted_pages: Option<&mut Vec>, + pagination_counter: &mut u64, + ) -> Result { + assert_eq!(PageNumber::serialized_size(), 8); // We assume below that PageNumber is length 8 + + system_tree.open_table_and_flush_table_root( + SYSTEM_FREED_TABLE.name(), + |system_freed_tree: &mut SystemFreedTree| { + while !system_freed_pages.lock().unwrap().is_empty() { + let chunk_size = 200; + let buffer_size = PageList::required_bytes(chunk_size); + let key = TransactionIdWithPagination { + transaction_id: self.transaction_id.raw_id(), + pagination_id: *pagination_counter, + }; + let mut access_guard = system_freed_tree.insert_reserve(&key, buffer_size)?; + + let mut freed_pages = system_freed_pages.lock().unwrap(); + let len = freed_pages.len(); + access_guard.as_mut().clear(); + for page in freed_pages.drain(len - min(len, chunk_size)..) { + if let Some(ref mut unpersisted_pages) = unpersisted_pages + && self.mem.unpersisted(page) + { + unpersisted_pages.push(page); + } else { + access_guard.as_mut().push_back(page); + } + } + drop(access_guard); + + *pagination_counter += 1; + } + Ok(()) + }, + )?; + + Ok(()) + } + + /// Retrieves information about storage usage in the database + pub fn stats(&self) -> Result { + let tables = self.tables.lock().unwrap(); + let table_tree = &tables.table_tree; + let data_tree_stats = table_tree.stats()?; + + let system_tables = self.system_tables.lock().unwrap(); + let system_table_tree = &system_tables.table_tree; + let system_tree_stats = system_table_tree.stats()?; + + let total_metadata_bytes = data_tree_stats.metadata_bytes() + + system_tree_stats.metadata_bytes + + system_tree_stats.stored_leaf_bytes; + let total_fragmented = data_tree_stats.fragmented_bytes() + + system_tree_stats.fragmented_bytes + + self.mem.count_free_pages()? * (self.mem.get_page_size() as u64); + + Ok(DatabaseStats { + tree_height: data_tree_stats.tree_height(), + allocated_pages: self.mem.count_allocated_pages()?, + leaf_pages: data_tree_stats.leaf_pages(), + branch_pages: data_tree_stats.branch_pages(), + stored_leaf_bytes: data_tree_stats.stored_bytes(), + metadata_bytes: total_metadata_bytes, + fragmented_bytes: total_fragmented, + page_size: self.mem.get_page_size(), + }) + } + + #[allow(dead_code)] + pub(crate) fn print_debug(&self) -> Result { + // Flush any pending updates to make sure we get the latest root + let mut tables = self.tables.lock().unwrap(); + if let Some(page) = tables + .table_tree + .flush_table_root_updates() + .unwrap() + .finalize_dirty_checksums() + .unwrap() + { + eprintln!("Master tree:"); + let master_tree: Btree<&str, InternalTableDefinition> = Btree::new( + Some(page), + PageHint::None, + self.transaction_guard.clone(), + self.mem.clone(), + )?; + master_tree.print_debug(true)?; + } + + // Flush any pending updates to make sure we get the latest root + let mut system_tables = self.system_tables.lock().unwrap(); + if let Some(page) = system_tables + .table_tree + .flush_table_root_updates() + .unwrap() + .finalize_dirty_checksums() + .unwrap() + { + eprintln!("System tree:"); + let master_tree: Btree<&str, InternalTableDefinition> = Btree::new( + Some(page), + PageHint::None, + self.transaction_guard.clone(), + self.mem.clone(), + )?; + master_tree.print_debug(true)?; + } + + Ok(()) + } +} + +impl Drop for WriteTransaction { + fn drop(&mut self) { + if !self.completed && !thread::panicking() && !self.mem.storage_failure() { + #[allow(unused_variables)] + if let Err(error) = self.abort_inner() { + #[cfg(feature = "logging")] + warn!("Failure automatically aborting transaction: {error}"); + } + } else if !self.completed && self.mem.storage_failure() { + self.tables + .lock() + .unwrap() + .table_tree + .clear_root_updates_and_close(); + } + } +} + +/// A read-only transaction +/// +/// Read-only transactions may exist concurrently with writes +pub struct ReadTransaction { + mem: Arc, + tree: TableTree, +} + +impl ReadTransaction { + pub(crate) fn new( + mem: Arc, + guard: TransactionGuard, + ) -> Result { + let root_page = mem.get_data_root(); + let guard = Arc::new(guard); + Ok(Self { + mem: mem.clone(), + tree: TableTree::new(root_page, PageHint::Clean, guard, mem) + .map_err(TransactionError::Storage)?, + }) + } + + /// Open the given table + pub fn open_table( + &self, + definition: TableDefinition, + ) -> Result, TableError> { + let header = self + .tree + .get_table::(definition.name(), TableType::Normal)? + .ok_or_else(|| TableError::TableDoesNotExist(definition.name().to_string()))?; + + match header { + InternalTableDefinition::Normal { table_root, .. } => Ok(ReadOnlyTable::new( + definition.name().to_string(), + table_root, + PageHint::Clean, + self.tree.transaction_guard().clone(), + self.mem.clone(), + )?), + InternalTableDefinition::Multimap { .. } => unreachable!(), + } + } + + /// Open the given table without a type + pub fn open_untyped_table( + &self, + handle: impl TableHandle, + ) -> Result { + let header = self + .tree + .get_table_untyped(handle.name(), TableType::Normal)? + .ok_or_else(|| TableError::TableDoesNotExist(handle.name().to_string()))?; + + match header { + InternalTableDefinition::Normal { + table_root, + fixed_key_size, + fixed_value_size, + .. + } => Ok(ReadOnlyUntypedTable::new( + table_root, + PageHint::Clean, + fixed_key_size, + fixed_value_size, + self.mem.clone(), + )), + InternalTableDefinition::Multimap { .. } => unreachable!(), + } + } + + /// Open the given table + pub fn open_multimap_table( + &self, + definition: MultimapTableDefinition, + ) -> Result, TableError> { + let header = self + .tree + .get_table::(definition.name(), TableType::Multimap)? + .ok_or_else(|| TableError::TableDoesNotExist(definition.name().to_string()))?; + + match header { + InternalTableDefinition::Normal { .. } => unreachable!(), + InternalTableDefinition::Multimap { + table_root, + table_length, + .. + } => Ok(ReadOnlyMultimapTable::new( + table_root, + table_length, + PageHint::Clean, + self.tree.transaction_guard().clone(), + self.mem.clone(), + )?), + } + } + + /// Open the given table without a type + pub fn open_untyped_multimap_table( + &self, + handle: impl MultimapTableHandle, + ) -> Result { + let header = self + .tree + .get_table_untyped(handle.name(), TableType::Multimap)? + .ok_or_else(|| TableError::TableDoesNotExist(handle.name().to_string()))?; + + match header { + InternalTableDefinition::Normal { .. } => unreachable!(), + InternalTableDefinition::Multimap { + table_root, + table_length, + fixed_key_size, + fixed_value_size, + .. + } => Ok(ReadOnlyUntypedMultimapTable::new( + table_root, + table_length, + PageHint::Clean, + fixed_key_size, + fixed_value_size, + self.mem.clone(), + )), + } + } + + /// List all the tables + pub fn list_tables(&self) -> Result> { + self.tree + .list_tables(TableType::Normal) + .map(|x| x.into_iter().map(UntypedTableHandle::new)) + } + + /// List all the multimap tables + pub fn list_multimap_tables(&self) -> Result> { + self.tree + .list_tables(TableType::Multimap) + .map(|x| x.into_iter().map(UntypedMultimapTableHandle::new)) + } + + /// Close the transaction + /// + /// Transactions are automatically closed when they and all objects referencing them have been dropped, + /// so this method does not normally need to be called. + /// This method can be used to ensure that there are no outstanding objects remaining. + /// + /// Returns `ReadTransactionStillInUse` error if a table or other object retrieved from the transaction still references this transaction + pub fn close(self) -> Result<(), TransactionError> { + if Arc::strong_count(self.tree.transaction_guard()) > 1 { + return Err(TransactionError::ReadTransactionStillInUse(Box::new(self))); + } + // No-op, just drop ourself + Ok(()) + } +} + +impl Debug for ReadTransaction { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str("ReadTransaction") + } +} + +#[cfg(test)] +mod test { + use crate::{Database, TableDefinition}; + + const X: TableDefinition<&str, &str> = TableDefinition::new("x"); + + #[test] + fn transaction_id_persistence() { + let tmpfile = crate::create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(X).unwrap(); + table.insert("hello", "world").unwrap(); + } + let first_txn_id = write_txn.transaction_id; + write_txn.commit().unwrap(); + drop(db); + + let db2 = Database::create(tmpfile.path()).unwrap(); + let write_txn = db2.begin_write().unwrap(); + assert!(write_txn.transaction_id > first_txn_id); + } +} diff --git a/v2/redb/src/tree_store/btree.rs b/v2/redb/src/tree_store/btree.rs new file mode 100644 index 000000000..757eb2dc9 --- /dev/null +++ b/v2/redb/src/tree_store/btree.rs @@ -0,0 +1,1120 @@ +use crate::db::TransactionGuard; +use crate::tree_store::btree_base::{ + AccessGuardMut, BRANCH, BranchAccessor, BranchMutator, BtreeHeader, Checksum, DEFERRED, LEAF, + LeafAccessor, branch_checksum, leaf_checksum, +}; +use crate::tree_store::btree_iters::BtreeExtractIf; +use crate::tree_store::btree_mutator::MutateHelper; +use crate::tree_store::page_store::{Page, PageImpl, PageMut, TransactionalMemory}; +use crate::tree_store::{ + AccessGuardMutInPlace, AllPageNumbersBtreeIter, BtreeRangeIter, PageHint, PageNumber, + PageTrackerPolicy, +}; +use crate::types::{Key, MutInPlaceValue, Value}; +use crate::{AccessGuard, Result}; +#[cfg(feature = "logging")] +use log::trace; +use std::borrow::Borrow; +use std::cmp::max; +use std::collections::HashMap; +use std::marker::PhantomData; +use std::ops::RangeBounds; +use std::sync::{Arc, Mutex}; + +pub(crate) struct BtreeStats { + pub(crate) tree_height: u32, + pub(crate) leaf_pages: u64, + pub(crate) branch_pages: u64, + pub(crate) stored_leaf_bytes: u64, + pub(crate) metadata_bytes: u64, + pub(crate) fragmented_bytes: u64, +} + +#[derive(Clone)] +pub(crate) struct PagePath { + path: Vec, +} + +impl PagePath { + pub(crate) fn new_root(page_number: PageNumber) -> Self { + Self { + path: vec![page_number], + } + } + + pub(crate) fn with_child(&self, page_number: PageNumber) -> Self { + let mut path = self.path.clone(); + path.push(page_number); + Self { path } + } + + pub(crate) fn with_subpath(&self, other: &Self) -> Self { + let mut path = self.path.clone(); + path.extend(&other.path); + Self { path } + } + + pub(crate) fn parents(&self) -> &[PageNumber] { + &self.path[..self.path.len() - 1] + } + + pub(crate) fn page_number(&self) -> PageNumber { + self.path[self.path.len() - 1] + } +} + +pub(crate) struct UntypedBtree { + mem: Arc, + root: Option, + hint: PageHint, + key_width: Option, + _value_width: Option, +} + +impl UntypedBtree { + pub(crate) fn new( + root: Option, + mem: Arc, + hint: PageHint, + key_width: Option, + value_width: Option, + ) -> Self { + Self { + mem, + root, + hint, + key_width, + _value_width: value_width, + } + } + + // Applies visitor to pages in the tree + pub(crate) fn visit_all_pages(&self, mut visitor: F) -> Result + where + F: FnMut(&PagePath) -> Result, + { + if let Some(page_number) = self.root.map(|x| x.root) { + self.visit_pages_helper(PagePath::new_root(page_number), &mut visitor)?; + } + + Ok(()) + } + + fn visit_pages_helper(&self, path: PagePath, visitor: &mut F) -> Result + where + F: FnMut(&PagePath) -> Result, + { + visitor(&path)?; + let page = self.mem.get_page(path.page_number(), self.hint)?; + + match page.memory()[0] { + LEAF => { + // No-op + } + BRANCH => { + let accessor = BranchAccessor::new(&page, self.key_width); + for i in 0..accessor.count_children() { + let child_page = accessor.child_page(i).unwrap(); + let child_path = path.with_child(child_page); + self.visit_pages_helper(child_path, visitor)?; + } + } + _ => unreachable!(), + } + + Ok(()) + } +} + +pub(crate) struct UntypedBtreeMut { + mem: Arc, + root: Option, + freed_pages: Arc>>, + key_width: Option, + value_width: Option, +} + +impl UntypedBtreeMut { + pub(crate) fn new( + root: Option, + mem: Arc, + freed_pages: Arc>>, + key_width: Option, + value_width: Option, + ) -> Self { + Self { + mem, + root, + freed_pages, + key_width, + value_width, + } + } + + pub(crate) fn get_root(&self) -> Option { + self.root + } + + // Recomputes the checksum for all pages that are uncommitted + pub(crate) fn finalize_dirty_checksums(&mut self) -> Result> { + let mut root = self.root; + if let Some(BtreeHeader { + root: ref p, + ref mut checksum, + length: _, + }) = root + { + if !self.mem.uncommitted(*p) { + // root page is clean + return Ok(root); + } + + *checksum = self.finalize_dirty_checksums_helper(*p)?; + self.root = root; + } + + Ok(root) + } + + fn finalize_dirty_checksums_helper(&mut self, page_number: PageNumber) -> Result { + assert!(self.mem.uncommitted(page_number)); + let mut page = self.mem.get_page_mut(page_number)?; + + match page.memory()[0] { + LEAF => leaf_checksum(&page, self.key_width, self.value_width), + BRANCH => { + let accessor = BranchAccessor::new(&page, self.key_width); + let mut new_children = vec![]; + for i in 0..accessor.count_children() { + let child_page = accessor.child_page(i).unwrap(); + if self.mem.uncommitted(child_page) { + let new_checksum = self.finalize_dirty_checksums_helper(child_page)?; + new_children.push(Some((i, child_page, new_checksum))); + } else { + // Child is clean, skip it + new_children.push(None); + } + } + + let mut mutator = BranchMutator::new(page.memory_mut()); + for (child_index, child_page, child_checksum) in new_children.into_iter().flatten() + { + mutator.write_child_page(child_index, child_page, child_checksum); + } + + branch_checksum(&page, self.key_width) + } + _ => unreachable!(), + } + } + + // Applies visitor to all dirty leaf pages in the tree + pub(crate) fn dirty_leaf_visitor(&mut self, visitor: F) -> Result + where + F: for<'a> Fn(PageMut<'a>) -> Result, + { + if let Some(page_number) = self.root.map(|x| x.root) { + if !self.mem.uncommitted(page_number) { + // root page is clean + return Ok(()); + } + + let page = self.mem.get_page_mut(page_number)?; + match page.memory()[0] { + LEAF => { + visitor(page)?; + } + BRANCH => { + drop(page); + self.dirty_leaf_visitor_helper(page_number, &visitor)?; + } + _ => unreachable!(), + } + } + + Ok(()) + } + + fn dirty_leaf_visitor_helper(&mut self, page_number: PageNumber, visitor: &F) -> Result + where + F: for<'a> Fn(PageMut<'a>) -> Result, + { + assert!(self.mem.uncommitted(page_number)); + let page = self.mem.get_page_mut(page_number)?; + + match page.memory()[0] { + LEAF => { + visitor(page)?; + } + BRANCH => { + let accessor = BranchAccessor::new(&page, self.key_width); + for i in 0..accessor.count_children() { + let child_page = accessor.child_page(i).unwrap(); + if self.mem.uncommitted(child_page) { + self.dirty_leaf_visitor_helper(child_page, visitor)?; + } + } + } + _ => unreachable!(), + } + + Ok(()) + } + + pub(crate) fn relocate( + &mut self, + relocation_map: &HashMap, + ) -> Result { + if let Some(root) = self.get_root() + && let Some((new_root, new_checksum)) = + self.relocate_helper(root.root, relocation_map)? + { + self.root = Some(BtreeHeader::new(new_root, new_checksum, root.length)); + return Ok(true); + } + Ok(false) + } + + // Relocates the given subtree to the pages specified in relocation_map + fn relocate_helper( + &mut self, + page_number: PageNumber, + relocation_map: &HashMap, + ) -> Result> { + let old_page = self.mem.get_page(page_number, PageHint::None)?; + let mut new_page = if let Some(new_page_number) = relocation_map.get(&page_number) { + self.mem.get_page_mut(*new_page_number)? + } else { + return Ok(None); + }; + new_page.memory_mut().copy_from_slice(old_page.memory()); + + let node_mem = old_page.memory(); + match node_mem[0] { + LEAF => { + // No-op + } + BRANCH => { + let accessor = BranchAccessor::new(&old_page, self.key_width); + let mut mutator = BranchMutator::new(new_page.memory_mut()); + for i in 0..accessor.count_children() { + let child = accessor.child_page(i).unwrap(); + if let Some((new_child, new_checksum)) = + self.relocate_helper(child, relocation_map)? + { + mutator.write_child_page(i, new_child, new_checksum); + } + } + } + _ => unreachable!(), + } + + let mut freed_pages = self.freed_pages.lock().unwrap(); + // No need to track allocations, because this method is only called during compaction when + // there can't be any savepoints + let mut ignore = PageTrackerPolicy::Ignore; + if !self.mem.free_if_uncommitted(page_number, &mut ignore) { + freed_pages.push(page_number); + } + + Ok(Some((new_page.get_page_number(), DEFERRED))) + } +} + +pub(crate) struct BtreeMut<'a, K: Key + 'static, V: Value + 'static> { + mem: Arc, + transaction_guard: Arc, + root: Option, + freed_pages: Arc>>, + allocated_pages: Arc>, + _key_type: PhantomData, + _value_type: PhantomData, + _lifetime: PhantomData<&'a ()>, +} + +impl BtreeMut<'_, K, V> { + pub(crate) fn new( + root: Option, + guard: Arc, + mem: Arc, + freed_pages: Arc>>, + allocated_pages: Arc>, + ) -> Self { + Self { + mem, + transaction_guard: guard, + root, + freed_pages, + allocated_pages, + _key_type: Default::default(), + _value_type: Default::default(), + _lifetime: Default::default(), + } + } + + pub(crate) fn finalize_dirty_checksums(&mut self) -> Result> { + let mut tree = UntypedBtreeMut::new( + self.get_root(), + self.mem.clone(), + self.freed_pages.clone(), + K::fixed_width(), + V::fixed_width(), + ); + self.root = tree.finalize_dirty_checksums()?; + Ok(self.root) + } + + #[allow(dead_code)] + pub(crate) fn all_pages_iter(&self) -> Result> { + if let Some(root) = self.root.map(|x| x.root) { + Ok(Some(AllPageNumbersBtreeIter::new( + root, + K::fixed_width(), + V::fixed_width(), + self.mem.clone(), + PageHint::None, + )?)) + } else { + Ok(None) + } + } + + pub(crate) fn visit_all_pages(&self, visitor: F) -> Result + where + F: FnMut(&PagePath) -> Result, + { + self.read_tree()?.visit_all_pages(visitor) + } + + pub(crate) fn get_root(&self) -> Option { + self.root + } + + pub(crate) fn set_root(&mut self, root: Option) { + self.root = root; + } + + pub(crate) fn relocate( + &mut self, + relocation_map: &HashMap, + ) -> Result { + let mut tree = UntypedBtreeMut::new( + self.get_root(), + self.mem.clone(), + self.freed_pages.clone(), + K::fixed_width(), + V::fixed_width(), + ); + if tree.relocate(relocation_map)? { + self.root = tree.get_root(); + Ok(true) + } else { + Ok(false) + } + } + + pub(crate) fn insert( + &mut self, + key: &K::SelfType<'_>, + value: &V::SelfType<'_>, + ) -> Result>> { + #[cfg(feature = "logging")] + trace!( + "Btree(root={:?}): Inserting {:?} with value of length {}", + &self.root, + key, + V::as_bytes(value).as_ref().len() + ); + let mut freed_pages = self.freed_pages.lock().unwrap(); + let mut operation: MutateHelper<'_, '_, K, V> = MutateHelper::new( + &mut self.root, + self.mem.clone(), + freed_pages.as_mut(), + self.allocated_pages.clone(), + ); + let (old_value, _) = operation.insert(key, value)?; + Ok(old_value) + } + + // Insert without allocating or freeing any pages. This requires that you've previously + // inserted the same key, with a value of at least the same serialized length, earlier + // in the same transaction. If those preconditions aren't satisfied, insert_inplace() + // will panic; it won't allocate under any circumstances + pub(crate) fn insert_inplace( + &mut self, + key: &K::SelfType<'_>, + value: &V::SelfType<'_>, + ) -> Result<()> { + let mut fake_freed_pages = vec![]; + let fake_allocated_pages = Arc::new(Mutex::new(PageTrackerPolicy::Closed)); + let mut operation = MutateHelper::::new( + &mut self.root, + self.mem.clone(), + fake_freed_pages.as_mut(), + fake_allocated_pages, + ); + operation.insert_inplace(key, value)?; + assert!(fake_freed_pages.is_empty()); + Ok(()) + } + + pub(crate) fn remove(&mut self, key: &K::SelfType<'_>) -> Result>> { + #[cfg(feature = "logging")] + trace!("Btree(root={:?}): Deleting {:?}", &self.root, key); + let mut freed_pages = self.freed_pages.lock().unwrap(); + let mut operation: MutateHelper<'_, '_, K, V> = MutateHelper::new( + &mut self.root, + self.mem.clone(), + freed_pages.as_mut(), + self.allocated_pages.clone(), + ); + let result = operation.delete(key)?; + Ok(result) + } + + #[allow(dead_code)] + pub(crate) fn print_debug(&self, include_values: bool) -> Result { + self.read_tree()?.print_debug(include_values) + } + + pub(crate) fn stats(&self) -> Result { + btree_stats( + self.get_root().map(|x| x.root), + &self.mem, + K::fixed_width(), + V::fixed_width(), + PageHint::None, + ) + } + + fn read_tree(&self) -> Result> { + Btree::new( + self.get_root(), + PageHint::None, + self.transaction_guard.clone(), + self.mem.clone(), + ) + } + + pub(crate) fn get(&self, key: &K::SelfType<'_>) -> Result>> { + self.read_tree()?.get(key) + } + + pub(crate) fn get_mut( + &mut self, + key: &K::SelfType<'_>, + ) -> Result>> { + if let Some(ref mut root) = self.root { + let key_bytes = K::as_bytes(key); + let query = key_bytes.as_ref(); + let page_mut = if self.mem.uncommitted(root.root) { + self.mem.get_page_mut(root.root)? + } else { + let mut freed_pages = self.freed_pages.lock().unwrap(); + let mut allocated = self.allocated_pages.lock().unwrap(); + let required: usize = root + .root + .page_size_bytes(self.mem.get_page_size().try_into().unwrap()) + .try_into() + .unwrap(); + let mut new_page = self.mem.allocate(required, &mut allocated)?; + let old_page = self.mem.get_page(root.root, PageHint::None)?; + new_page.memory_mut().copy_from_slice(old_page.memory()); + drop(old_page); + freed_pages.push(root.root); + + root.root = new_page.get_page_number(); + root.checksum = DEFERRED; + new_page + }; + self.get_mut_helper(None, page_mut, query) + } else { + Ok(None) + } + } + + fn get_mut_helper<'txn>( + &'txn mut self, + parent: Option<(PageMut<'txn>, usize)>, + mut page: PageMut<'txn>, + query: &[u8], + ) -> Result>> { + let node_mem = page.memory(); + match node_mem[0] { + LEAF => { + let accessor = LeafAccessor::new(page.memory(), K::fixed_width(), V::fixed_width()); + if let Some(entry_index) = accessor.find_key::(query) { + let (start, end) = accessor.value_range(entry_index).unwrap(); + let guard = AccessGuardMut::new( + page, + start, + end - start, + entry_index, + parent, + self.mem.clone(), + self.allocated_pages.clone(), + self.root.as_mut().unwrap(), + K::fixed_width(), + ); + Ok(Some(guard)) + } else { + Ok(None) + } + } + BRANCH => { + let (child_index, child_page) = { + let accessor = BranchAccessor::new(&page, K::fixed_width()); + accessor.child_for_key::(query) + }; + let child_page_mut = if self.mem.uncommitted(child_page) { + self.mem.get_page_mut(child_page)? + } else { + let mut freed_pages = self.freed_pages.lock().unwrap(); + let mut allocated = self.allocated_pages.lock().unwrap(); + let required: usize = child_page + .page_size_bytes(self.mem.get_page_size().try_into().unwrap()) + .try_into() + .unwrap(); + let mut new_page = self.mem.allocate(required, &mut allocated)?; + let old_child_page = self.mem.get_page(child_page, PageHint::None)?; + new_page + .memory_mut() + .copy_from_slice(old_child_page.memory()); + drop(old_child_page); + freed_pages.push(child_page); + + let mut mutator = BranchMutator::new(page.memory_mut()); + mutator.write_child_page(child_index, new_page.get_page_number(), DEFERRED); + new_page + }; + self.get_mut_helper(Some((page, child_index)), child_page_mut, query) + } + _ => unreachable!(), + } + } + + pub(crate) fn first( + &self, + ) -> Result, AccessGuard<'static, V>)>> { + self.read_tree()?.first() + } + + pub(crate) fn last( + &self, + ) -> Result, AccessGuard<'static, V>)>> { + self.read_tree()?.last() + } + + pub(crate) fn range<'a0, T: RangeBounds + 'a0, KR: Borrow> + 'a0>( + &self, + range: &'_ T, + ) -> Result> + where + K: 'a0, + { + self.read_tree()?.range(range) + } + + pub(crate) fn extract_from_if< + 'a, + 'a0, + T: RangeBounds + 'a0, + KR: Borrow> + 'a0, + F: for<'f> FnMut(K::SelfType<'f>, V::SelfType<'f>) -> bool, + >( + &'a mut self, + range: &'_ T, + predicate: F, + ) -> Result> + where + K: 'a0, + { + let iter = self.range(range)?; + + let result = BtreeExtractIf::new( + &mut self.root, + iter, + predicate, + self.freed_pages.clone(), + self.allocated_pages.clone(), + self.mem.clone(), + ); + + Ok(result) + } + + pub(crate) fn retain_in<'a, KR, F: for<'f> FnMut(K::SelfType<'f>, V::SelfType<'f>) -> bool>( + &mut self, + mut predicate: F, + range: impl RangeBounds + 'a, + ) -> Result + where + KR: Borrow> + 'a, + { + let iter = self.range(&range)?; + let mut freed = vec![]; + // Do not modify the existing tree, because we're iterating over it concurrently with the removals + // TODO: optimize this to iterate and remove at the same time + let mut operation: MutateHelper<'_, '_, K, V> = MutateHelper::new_do_not_modify( + &mut self.root, + self.mem.clone(), + &mut freed, + self.allocated_pages.clone(), + ); + for entry in iter { + let entry = entry?; + if !predicate(entry.key(), entry.value()) { + assert!(operation.delete(&entry.key())?.is_some()); + } + } + let mut freed_pages = self.freed_pages.lock().unwrap(); + let mut allocated_pages = self.allocated_pages.lock().unwrap(); + for page in freed { + if !self.mem.free_if_uncommitted(page, &mut allocated_pages) { + freed_pages.push(page); + } + } + + Ok(()) + } + + pub(crate) fn len(&self) -> Result { + self.read_tree()?.len() + } +} + +impl<'a, K: Key + 'a, V: MutInPlaceValue + 'a> BtreeMut<'a, K, V> { + /// Reserve space to insert a key-value pair + /// The returned reference will have length equal to `value_length` + // Return type has the same lifetime as &self, because the tree must not be modified until the mutable guard is dropped + pub(crate) fn insert_reserve( + &mut self, + key: &K::SelfType<'_>, + value_length: usize, + ) -> Result> { + #[cfg(feature = "logging")] + trace!( + "Btree(root={:?}): Inserting {:?} with {} reserved bytes for the value", + &self.root, key, value_length + ); + let mut freed_pages = self.freed_pages.lock().unwrap(); + let mut value = vec![0u8; value_length]; + V::initialize(&mut value); + let mut operation = MutateHelper::::new( + &mut self.root, + self.mem.clone(), + freed_pages.as_mut(), + self.allocated_pages.clone(), + ); + let (_, guard) = operation.insert(key, &V::from_bytes(&value))?; + Ok(guard) + } +} + +pub(crate) struct RawBtree { + mem: Arc, + root: Option, + hint: PageHint, + fixed_key_size: Option, + fixed_value_size: Option, +} + +impl RawBtree { + pub(crate) fn new( + root: Option, + fixed_key_size: Option, + fixed_value_size: Option, + mem: Arc, + hint: PageHint, + ) -> Self { + Self { + mem, + root, + hint, + fixed_key_size, + fixed_value_size, + } + } + + pub(crate) fn get_root(&self) -> Option { + self.root + } + + pub(crate) fn stats(&self) -> Result { + btree_stats( + self.root.map(|x| x.root), + &self.mem, + self.fixed_key_size, + self.fixed_value_size, + self.hint, + ) + } + + pub(crate) fn len(&self) -> Result { + Ok(self.root.map_or(0, |x| x.length)) + } + + pub(crate) fn verify_checksum(&self) -> Result { + if let Some(header) = self.root { + self.verify_checksum_helper(header.root, header.checksum) + } else { + Ok(true) + } + } + + fn verify_checksum_helper( + &self, + page_number: PageNumber, + expected_checksum: Checksum, + ) -> Result { + let page = self.mem.get_page(page_number, self.hint)?; + let node_mem = page.memory(); + Ok(match node_mem[0] { + LEAF => { + if let Ok(computed) = + leaf_checksum(&page, self.fixed_key_size, self.fixed_value_size) + { + expected_checksum == computed + } else { + false + } + } + BRANCH => { + if let Ok(computed) = branch_checksum(&page, self.fixed_key_size) { + if expected_checksum != computed { + return Ok(false); + } + } else { + return Ok(false); + } + let accessor = BranchAccessor::new(&page, self.fixed_key_size); + for i in 0..accessor.count_children() { + if !self.verify_checksum_helper( + accessor.child_page(i).unwrap(), + accessor.child_checksum(i).unwrap(), + )? { + return Ok(false); + } + } + true + } + _ => false, + }) + } +} + +pub(crate) struct Btree { + mem: Arc, + transaction_guard: Arc, + // Cache of the root page to avoid repeated lookups + cached_root: Option, + root: Option, + hint: PageHint, + _key_type: PhantomData, + _value_type: PhantomData, +} + +impl Btree { + pub(crate) fn new( + root: Option, + hint: PageHint, + guard: Arc, + mem: Arc, + ) -> Result { + let cached_root = if let Some(header) = root { + Some(mem.get_page(header.root, hint)?) + } else { + None + }; + Ok(Self { + mem, + transaction_guard: guard, + cached_root, + root, + hint, + _key_type: Default::default(), + _value_type: Default::default(), + }) + } + + pub(crate) fn transaction_guard(&self) -> &Arc { + &self.transaction_guard + } + + pub(crate) fn hint(&self) -> PageHint { + self.hint + } + + pub(crate) fn get_root(&self) -> Option { + self.root + } + + pub(crate) fn verify_checksum(&self) -> Result { + RawBtree::new( + self.get_root(), + K::fixed_width(), + V::fixed_width(), + self.mem.clone(), + self.hint, + ) + .verify_checksum() + } + + pub(crate) fn visit_all_pages(&self, visitor: F) -> Result + where + F: FnMut(&PagePath) -> Result, + { + let tree = UntypedBtree::new( + self.root, + self.mem.clone(), + self.hint, + K::fixed_width(), + V::fixed_width(), + ); + tree.visit_all_pages(visitor) + } + + pub(crate) fn get(&self, key: &K::SelfType<'_>) -> Result>> { + if let Some(ref root_page) = self.cached_root { + self.get_helper(root_page, K::as_bytes(key).as_ref()) + } else { + Ok(None) + } + } + + // Returns the value for the queried key, if present + fn get_helper(&self, page: &PageImpl, query: &[u8]) -> Result>> { + let node_mem = page.memory(); + match node_mem[0] { + LEAF => { + let accessor = LeafAccessor::new(page.memory(), K::fixed_width(), V::fixed_width()); + if let Some(entry_index) = accessor.find_key::(query) { + let (start, end) = accessor.value_range(entry_index).unwrap(); + let guard = AccessGuard::with_page(page.clone(), start..end); + Ok(Some(guard)) + } else { + Ok(None) + } + } + BRANCH => { + let accessor = BranchAccessor::new(page, K::fixed_width()); + let (_, child_page) = accessor.child_for_key::(query); + let child_page = self.mem.get_page(child_page, self.hint)?; + self.get_helper(&child_page, query) + } + _ => unreachable!(), + } + } + + pub(crate) fn first( + &self, + ) -> Result, AccessGuard<'static, V>)>> { + if let Some(ref root) = self.cached_root { + self.first_helper(root.clone()) + } else { + Ok(None) + } + } + + fn first_helper( + &self, + page: PageImpl, + ) -> Result, AccessGuard<'static, V>)>> { + let node_mem = page.memory(); + match node_mem[0] { + LEAF => { + let accessor = LeafAccessor::new(page.memory(), K::fixed_width(), V::fixed_width()); + let (key_range, value_range) = accessor.entry_ranges(0).unwrap(); + let key_guard = AccessGuard::with_page(page.clone(), key_range); + let value_guard = AccessGuard::with_page(page, value_range); + Ok(Some((key_guard, value_guard))) + } + BRANCH => { + let accessor = BranchAccessor::new(&page, K::fixed_width()); + let child_page = accessor.child_page(0).unwrap(); + self.first_helper(self.mem.get_page(child_page, self.hint)?) + } + _ => unreachable!(), + } + } + + pub(crate) fn last( + &self, + ) -> Result, AccessGuard<'static, V>)>> { + if let Some(ref root) = self.cached_root { + self.last_helper(root.clone()) + } else { + Ok(None) + } + } + + fn last_helper( + &self, + page: PageImpl, + ) -> Result, AccessGuard<'static, V>)>> { + let node_mem = page.memory(); + match node_mem[0] { + LEAF => { + let accessor = LeafAccessor::new(page.memory(), K::fixed_width(), V::fixed_width()); + let (key_range, value_range) = + accessor.entry_ranges(accessor.num_pairs() - 1).unwrap(); + let key_guard = AccessGuard::with_page(page.clone(), key_range); + let value_guard = AccessGuard::with_page(page, value_range); + Ok(Some((key_guard, value_guard))) + } + BRANCH => { + let accessor = BranchAccessor::new(&page, K::fixed_width()); + let child_page = accessor.child_page(accessor.count_children() - 1).unwrap(); + self.last_helper(self.mem.get_page(child_page, self.hint)?) + } + _ => unreachable!(), + } + } + + pub(crate) fn range<'a0, T: RangeBounds, KR: Borrow>>( + &self, + range: &'_ T, + ) -> Result> { + BtreeRangeIter::new( + range, + self.root.map(|x| x.root), + self.mem.clone(), + self.hint, + ) + } + + pub(crate) fn len(&self) -> Result { + Ok(self.root.map_or(0, |x| x.length)) + } + + pub(crate) fn stats(&self) -> Result { + btree_stats( + self.root.map(|x| x.root), + &self.mem, + K::fixed_width(), + V::fixed_width(), + self.hint, + ) + } + + #[allow(dead_code)] + pub(crate) fn print_debug(&self, include_values: bool) -> Result { + if let Some(p) = self.root.map(|x| x.root) { + let mut pages = vec![self.mem.get_page(p, self.hint)?]; + while !pages.is_empty() { + let mut next_children = vec![]; + for page in pages.drain(..) { + let node_mem = page.memory(); + match node_mem[0] { + LEAF => { + eprint!("Leaf[ (page={:?})", page.get_page_number()); + LeafAccessor::new(page.memory(), K::fixed_width(), V::fixed_width()) + .print_node::(include_values); + eprint!("]"); + } + BRANCH => { + let accessor = BranchAccessor::new(&page, K::fixed_width()); + for i in 0..accessor.count_children() { + let child = accessor.child_page(i).unwrap(); + next_children.push(self.mem.get_page(child, self.hint)?); + } + accessor.print_node::(); + } + _ => unreachable!(), + } + eprint!(" "); + } + eprintln!(); + + pages = next_children; + } + } + + Ok(()) + } +} + +impl Drop for Btree { + fn drop(&mut self) { + // Make sure that we clear our reference to the root page, before the transaction guard goes out of scope + self.cached_root = None; + } +} + +pub(crate) fn btree_stats( + root: Option, + mem: &TransactionalMemory, + fixed_key_size: Option, + fixed_value_size: Option, + hint: PageHint, +) -> Result { + if let Some(root) = root { + stats_helper(root, mem, fixed_key_size, fixed_value_size, hint) + } else { + Ok(BtreeStats { + tree_height: 0, + leaf_pages: 0, + branch_pages: 0, + stored_leaf_bytes: 0, + metadata_bytes: 0, + fragmented_bytes: 0, + }) + } +} + +fn stats_helper( + page_number: PageNumber, + mem: &TransactionalMemory, + fixed_key_size: Option, + fixed_value_size: Option, + hint: PageHint, +) -> Result { + let page = mem.get_page(page_number, hint)?; + let node_mem = page.memory(); + match node_mem[0] { + LEAF => { + let accessor = LeafAccessor::new(page.memory(), fixed_key_size, fixed_value_size); + let leaf_bytes = accessor.length_of_pairs(0, accessor.num_pairs()); + let overhead_bytes = accessor.total_length() - leaf_bytes; + let fragmented_bytes = (page.memory().len() - accessor.total_length()) as u64; + Ok(BtreeStats { + tree_height: 1, + leaf_pages: 1, + branch_pages: 0, + stored_leaf_bytes: leaf_bytes.try_into().unwrap(), + metadata_bytes: overhead_bytes.try_into().unwrap(), + fragmented_bytes, + }) + } + BRANCH => { + let accessor = BranchAccessor::new(&page, fixed_key_size); + let mut max_child_height = 0; + let mut leaf_pages = 0; + let mut branch_pages = 1; + let mut stored_leaf_bytes = 0; + let mut metadata_bytes = accessor.total_length() as u64; + let mut fragmented_bytes = (page.memory().len() - accessor.total_length()) as u64; + for i in 0..accessor.count_children() { + if let Some(child) = accessor.child_page(i) { + let stats = stats_helper(child, mem, fixed_key_size, fixed_value_size, hint)?; + max_child_height = max(max_child_height, stats.tree_height); + leaf_pages += stats.leaf_pages; + branch_pages += stats.branch_pages; + stored_leaf_bytes += stats.stored_leaf_bytes; + metadata_bytes += stats.metadata_bytes; + fragmented_bytes += stats.fragmented_bytes; + } + } + + Ok(BtreeStats { + tree_height: max_child_height + 1, + leaf_pages, + branch_pages, + stored_leaf_bytes, + metadata_bytes, + fragmented_bytes, + }) + } + _ => unreachable!(), + } +} diff --git a/v2/redb/src/tree_store/btree_base.rs b/v2/redb/src/tree_store/btree_base.rs new file mode 100644 index 000000000..25ddc607c --- /dev/null +++ b/v2/redb/src/tree_store/btree_base.rs @@ -0,0 +1,1725 @@ +use crate::tree_store::page_store::{Page, PageImpl, PageMut, TransactionalMemory, xxh3_checksum}; +use crate::tree_store::{PageNumber, PageTrackerPolicy}; +use crate::types::{Key, MutInPlaceValue, Value}; +use crate::{Result, StorageError}; +use std::borrow::Borrow; +use std::cmp::Ordering; +use std::marker::PhantomData; +use std::mem::size_of; +use std::ops::Range; +use std::sync::{Arc, Mutex}; +use std::thread; + +pub(crate) const LEAF: u8 = 1; +pub(crate) const BRANCH: u8 = 2; + +pub(crate) type Checksum = u128; +// Dummy value. Final value will be computed during commit +pub(crate) const DEFERRED: Checksum = 999; + +pub(super) fn leaf_checksum( + page: &T, + fixed_key_size: Option, + fixed_value_size: Option, +) -> Result { + let accessor = LeafAccessor::new(page.memory(), fixed_key_size, fixed_value_size); + let last_pair = accessor.num_pairs().checked_sub(1).ok_or_else(|| { + StorageError::Corrupted(format!( + "Leaf page {:?} corrupted. Number of pairs is zero", + page.get_page_number() + )) + })?; + let end = accessor.value_end(last_pair).ok_or_else(|| { + StorageError::Corrupted(format!( + "Leaf page {:?} corrupted. Couldn't find offset for pair {}", + page.get_page_number(), + last_pair, + )) + })?; + if end > page.memory().len() { + Err(StorageError::Corrupted(format!( + "Leaf page {:?} corrupted. Last offset {} beyond end of data {}", + page.get_page_number(), + end, + page.memory().len() + ))) + } else { + Ok(xxh3_checksum(&page.memory()[..end])) + } +} + +pub(super) fn branch_checksum( + page: &T, + fixed_key_size: Option, +) -> Result { + let accessor = BranchAccessor::new(page, fixed_key_size); + let last_key = accessor.num_keys().checked_sub(1).ok_or_else(|| { + StorageError::Corrupted(format!( + "Branch page {:?} corrupted. Number of keys is zero", + page.get_page_number() + )) + })?; + let end = accessor.key_end(last_key).ok_or_else(|| { + StorageError::Corrupted(format!( + "Branch page {:?} corrupted. Can't find offset for key {}", + page.get_page_number(), + last_key + )) + })?; + if end > page.memory().len() { + Err(StorageError::Corrupted(format!( + "Branch page {:?} corrupted. Last offset {} beyond end of data {}", + page.get_page_number(), + end, + page.memory().len() + ))) + } else { + Ok(xxh3_checksum(&page.memory()[..end])) + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub(crate) struct BtreeHeader { + pub(crate) root: PageNumber, + pub(crate) checksum: Checksum, + pub(crate) length: u64, +} + +impl BtreeHeader { + pub(crate) fn new(root: PageNumber, checksum: Checksum, length: u64) -> Self { + Self { + root, + checksum, + length, + } + } + + pub(crate) const fn serialized_size() -> usize { + PageNumber::serialized_size() + size_of::() + size_of::() + } + + pub(crate) fn from_le_bytes(bytes: [u8; Self::serialized_size()]) -> Self { + let root = + PageNumber::from_le_bytes(bytes[..PageNumber::serialized_size()].try_into().unwrap()); + let mut offset = PageNumber::serialized_size(); + let checksum = Checksum::from_le_bytes( + bytes[offset..(offset + size_of::())] + .try_into() + .unwrap(), + ); + offset += size_of::(); + let length = u64::from_le_bytes( + bytes[offset..(offset + size_of::())] + .try_into() + .unwrap(), + ); + + Self { + root, + checksum, + length, + } + } + + pub(crate) fn to_le_bytes(self) -> [u8; Self::serialized_size()] { + let mut result = [0; Self::serialized_size()]; + result[..PageNumber::serialized_size()].copy_from_slice(&self.root.to_le_bytes()); + result[PageNumber::serialized_size() + ..(PageNumber::serialized_size() + size_of::())] + .copy_from_slice(&self.checksum.to_le_bytes()); + result[(PageNumber::serialized_size() + size_of::())..] + .copy_from_slice(&self.length.to_le_bytes()); + + result + } +} + +enum OnDrop { + None, + RemoveEntry { + position: usize, + fixed_key_size: Option, + }, +} + +enum EitherPage<'txn> { + Immutable(PageImpl), + Mutable(PageMut<'txn>), + OwnedMemory(Vec), + ArcMemory(Arc<[u8]>), +} + +impl EitherPage<'_> { + fn memory(&self) -> &[u8] { + match self { + EitherPage::Immutable(page) => page.memory(), + EitherPage::Mutable(page) => page.memory(), + EitherPage::OwnedMemory(mem) => mem.as_slice(), + EitherPage::ArcMemory(mem) => mem, + } + } +} + +/// Scoped accessor to data in the database +/// +/// When this structure is dropped (goes out of scope), the data is released +pub struct AccessGuard<'a, V: Value + 'static> { + page: EitherPage<'a>, + offset: usize, + len: usize, + on_drop: OnDrop, + _value_type: PhantomData, +} + +impl<'a, V: Value + 'static> AccessGuard<'a, V> { + pub(crate) fn with_page(page: PageImpl, range: Range) -> Self { + Self { + page: EitherPage::Immutable(page), + offset: range.start, + len: range.len(), + on_drop: OnDrop::None, + _value_type: Default::default(), + } + } + + pub(crate) fn with_arc_page(page: Arc<[u8]>, range: Range) -> Self { + Self { + page: EitherPage::ArcMemory(page), + offset: range.start, + len: range.len(), + on_drop: OnDrop::None, + _value_type: Default::default(), + } + } + + pub(crate) fn with_owned_value(value: Vec) -> Self { + let len = value.len(); + Self { + page: EitherPage::OwnedMemory(value), + offset: 0, + len, + on_drop: OnDrop::None, + _value_type: Default::default(), + } + } + + pub(super) fn remove_on_drop( + page: PageMut<'a>, + offset: usize, + len: usize, + position: usize, + fixed_key_size: Option, + ) -> Self { + Self { + page: EitherPage::Mutable(page), + offset, + len, + on_drop: OnDrop::RemoveEntry { + position, + fixed_key_size, + }, + _value_type: Default::default(), + } + } + + /// Access the stored value + pub fn value(&self) -> V::SelfType<'_> { + V::from_bytes(&self.page.memory()[self.offset..(self.offset + self.len)]) + } +} + +impl Drop for AccessGuard<'_, V> { + fn drop(&mut self) { + match self.on_drop { + OnDrop::None => {} + OnDrop::RemoveEntry { + position, + fixed_key_size, + } => { + if let EitherPage::Mutable(ref mut mut_page) = self.page { + let mut mutator = + LeafMutator::new(mut_page.memory_mut(), fixed_key_size, V::fixed_width()); + mutator.remove(position); + } else if !thread::panicking() { + unreachable!(); + } + } + } + } +} + +pub struct AccessGuardMut<'a, V: Value + 'static> { + page: PageMut<'a>, + offset: usize, + len: usize, + entry_index: usize, + parent: Option<(PageMut<'a>, usize)>, + mem: Arc, + allocated: Arc>, + root_ref: &'a mut BtreeHeader, + key_width: Option, + _value_type: PhantomData, +} + +impl<'a, V: Value + 'static> AccessGuardMut<'a, V> { + #[allow(clippy::too_many_arguments)] + pub(crate) fn new( + page: PageMut<'a>, + offset: usize, + len: usize, + entry_index: usize, + parent: Option<(PageMut<'a>, usize)>, + mem: Arc, + allocated: Arc>, + root_ref: &'a mut BtreeHeader, + key_width: Option, + ) -> Self { + assert!(mem.uncommitted(page.get_page_number())); + if let Some((ref parent_page, _)) = parent { + assert!(mem.uncommitted(parent_page.get_page_number())); + } + AccessGuardMut { + page, + offset, + len, + entry_index, + parent, + mem, + allocated, + root_ref, + key_width, + _value_type: Default::default(), + } + } + + /// Access the stored value + pub fn value(&self) -> V::SelfType<'_> { + V::from_bytes(&self.page.memory()[self.offset..(self.offset + self.len)]) + } + + /// Replace the stored value + pub fn insert<'v>(&mut self, value: impl Borrow>) -> Result<()> { + let value_bytes = V::as_bytes(value.borrow()); + + if LeafMutator::sufficient_replace_inplace_space( + &self.page, + self.entry_index, + self.key_width, + V::fixed_width(), + value_bytes.as_ref(), + ) { + let mut mutator = + LeafMutator::new(self.page.memory_mut(), self.key_width, V::fixed_width()); + mutator.replace(self.entry_index, value_bytes.as_ref()); + } else { + let accessor = LeafAccessor::new(self.page.memory(), self.key_width, V::fixed_width()); + let mut builder = LeafBuilder::new( + &self.mem, + &self.allocated, + accessor.num_pairs(), + self.key_width, + V::fixed_width(), + ); + + for i in 0..accessor.num_pairs() { + let entry = accessor.entry(i).unwrap(); + if i == self.entry_index { + builder.push(entry.key(), value_bytes.as_ref()); + } else { + builder.push(entry.key(), entry.value()); + } + } + + let new_page = builder.build()?; + + // Update parent branch page if it exists, otherwise update root + if let Some((ref mut parent_page, parent_entry_index)) = self.parent { + let mut mutator = BranchMutator::new(parent_page.memory_mut()); + mutator.write_child_page(parent_entry_index, new_page.get_page_number(), DEFERRED); + } else { + self.root_ref.root = new_page.get_page_number(); + self.root_ref.checksum = DEFERRED; + } + + let old_page_number = self.page.get_page_number(); + self.page = new_page; + let mut allocated = self.allocated.lock().unwrap(); + assert!( + self.mem + .free_if_uncommitted(old_page_number, &mut allocated) + ); + } + + // Update our page reference to the new page and recalculate offset/length + let new_accessor = LeafAccessor::new(self.page.memory(), self.key_width, V::fixed_width()); + let (new_start, new_end) = new_accessor.value_range(self.entry_index).unwrap(); + + self.offset = new_start; + self.len = new_end - new_start; + + Ok(()) + } +} + +impl Drop for AccessGuardMut<'_, V> { + fn drop(&mut self) { + // no-op. This Drop impl is only here to ensure that self is dropped before the transaction + // is committed. (i.e. that the lifetime 'a is shorter than the transaction) + } +} + +pub struct AccessGuardMutInPlace<'a, V: Value + 'static> { + page: PageMut<'a>, + offset: usize, + len: usize, + _value_type: PhantomData, +} + +impl<'a, V: Value + 'static> AccessGuardMutInPlace<'a, V> { + pub(crate) fn new(page: PageMut<'a>, offset: usize, len: usize) -> Self { + AccessGuardMutInPlace { + page, + offset, + len, + _value_type: Default::default(), + } + } +} + +impl AsMut for AccessGuardMutInPlace<'_, V> { + fn as_mut(&mut self) -> &mut V::BaseRefType { + V::from_bytes_mut(&mut self.page.memory_mut()[self.offset..(self.offset + self.len)]) + } +} + +impl Drop for AccessGuardMutInPlace<'_, V> { + fn drop(&mut self) { + // no-op. This Drop impl is only here to ensure that self is dropped before the transaction + // is committed. (i.e. that the lifetime 'a is shorter than the transaction) + } +} + +// Provides a simple zero-copy way to access entries +pub struct EntryAccessor<'a> { + key: &'a [u8], + value: &'a [u8], +} + +impl<'a> EntryAccessor<'a> { + fn new(key: &'a [u8], value: &'a [u8]) -> Self { + EntryAccessor { key, value } + } +} + +impl<'a: 'b, 'b> EntryAccessor<'a> { + pub(crate) fn key(&'b self) -> &'a [u8] { + self.key + } + + pub(crate) fn value(&'b self) -> &'a [u8] { + self.value + } +} + +// Provides a simple zero-copy way to access a leaf page +pub(crate) struct LeafAccessor<'a> { + page: &'a [u8], + fixed_key_size: Option, + fixed_value_size: Option, + num_pairs: usize, +} + +impl<'a> LeafAccessor<'a> { + pub(crate) fn new( + page: &'a [u8], + fixed_key_size: Option, + fixed_value_size: Option, + ) -> Self { + debug_assert_eq!(page[0], LEAF); + let num_pairs = u16::from_le_bytes(page[2..4].try_into().unwrap()) as usize; + LeafAccessor { + page, + fixed_key_size, + fixed_value_size, + num_pairs, + } + } + + pub(super) fn print_node(&self, include_value: bool) { + let mut i = 0; + while let Some(entry) = self.entry(i) { + eprint!(" key_{i}={:?}", K::from_bytes(entry.key())); + if include_value { + eprint!(" value_{i}={:?}", V::from_bytes(entry.value())); + } + i += 1; + } + } + + pub(crate) fn position(&self, query: &[u8]) -> (usize, bool) { + // inclusive + let mut min_entry = 0; + // inclusive. Start past end, since it might be positioned beyond the end of the leaf + let mut max_entry = self.num_pairs(); + while min_entry < max_entry { + let mid = min_entry.midpoint(max_entry); + let key = self.key_unchecked(mid); + match K::compare(query, key) { + Ordering::Less => { + max_entry = mid; + } + Ordering::Equal => { + return (mid, true); + } + Ordering::Greater => { + min_entry = mid + 1; + } + } + } + debug_assert_eq!(min_entry, max_entry); + (min_entry, false) + } + + pub(crate) fn find_key(&self, query: &[u8]) -> Option { + let (entry, found) = self.position::(query); + if found { Some(entry) } else { None } + } + + fn key_section_start(&self) -> usize { + let mut offset = 4; + if self.fixed_key_size.is_none() { + offset += size_of::() * self.num_pairs; + } + if self.fixed_value_size.is_none() { + offset += size_of::() * self.num_pairs; + } + + offset + } + + fn key_start(&self, n: usize) -> Option { + if n == 0 { + Some(self.key_section_start()) + } else { + self.key_end(n - 1) + } + } + + fn key_end(&self, n: usize) -> Option { + if n >= self.num_pairs() { + None + } else { + if let Some(fixed) = self.fixed_key_size { + return Some(self.key_section_start() + fixed * (n + 1)); + } + let offset = 4 + size_of::() * n; + let end = u32::from_le_bytes( + self.page + .get(offset..(offset + size_of::()))? + .try_into() + .unwrap(), + ) as usize; + Some(end) + } + } + + fn value_start(&self, n: usize) -> Option { + if n == 0 { + self.key_end(self.num_pairs() - 1) + } else { + self.value_end(n - 1) + } + } + + fn value_end(&self, n: usize) -> Option { + if n >= self.num_pairs() { + None + } else { + if let Some(fixed) = self.fixed_value_size { + return Some(self.key_end(self.num_pairs.checked_sub(1)?)? + fixed * (n + 1)); + } + let mut offset = 4 + size_of::() * n; + if self.fixed_key_size.is_none() { + offset += size_of::() * self.num_pairs; + } + let end = u32::from_le_bytes( + self.page + .get(offset..(offset + size_of::()))? + .try_into() + .unwrap(), + ) as usize; + Some(end) + } + } + + pub(crate) fn num_pairs(&self) -> usize { + self.num_pairs + } + + pub(super) fn offset_of_first_value(&self) -> usize { + self.offset_of_value(0).unwrap() + } + + pub(super) fn offset_of_value(&self, n: usize) -> Option { + self.value_start(n) + } + + pub(super) fn value_range(&self, n: usize) -> Option<(usize, usize)> { + Some((self.value_start(n)?, self.value_end(n)?)) + } + + // Returns the length of all keys and values between [start, end) + pub(crate) fn length_of_pairs(&self, start: usize, end: usize) -> usize { + self.length_of_values(start, end) + self.length_of_keys(start, end) + } + + fn length_of_values(&self, start: usize, end: usize) -> usize { + if end == 0 { + return 0; + } + let end_offset = self.value_end(end - 1).unwrap(); + let start_offset = self.value_start(start).unwrap(); + end_offset - start_offset + } + + // Returns the length of all keys between [start, end) + pub(crate) fn length_of_keys(&self, start: usize, end: usize) -> usize { + if end == 0 { + return 0; + } + let end_offset = self.key_end(end - 1).unwrap(); + let start_offset = self.key_start(start).unwrap(); + end_offset - start_offset + } + + pub(crate) fn total_length(&self) -> usize { + // Values are stored last + self.value_end(self.num_pairs() - 1).unwrap() + } + + fn key_unchecked(&self, n: usize) -> &[u8] { + &self.page[self.key_start(n).unwrap()..self.key_end(n).unwrap()] + } + + pub(crate) fn entry(&self, n: usize) -> Option> { + let key = &self.page[self.key_start(n)?..self.key_end(n)?]; + let value = &self.page[self.value_start(n)?..self.value_end(n)?]; + Some(EntryAccessor::new(key, value)) + } + + pub(crate) fn entry_ranges(&self, n: usize) -> Option<(Range, Range)> { + let key = self.key_start(n)?..self.key_end(n)?; + let value = self.value_start(n)?..self.value_end(n)?; + Some((key, value)) + } + + pub(super) fn last_entry(&self) -> EntryAccessor<'a> { + self.entry(self.num_pairs() - 1).unwrap() + } +} + +pub(super) struct LeafBuilder<'a, 'b> { + pairs: Vec<(&'a [u8], &'a [u8])>, + fixed_key_size: Option, + fixed_value_size: Option, + total_key_bytes: usize, + total_value_bytes: usize, + mem: &'b TransactionalMemory, + allocated_pages: &'b Mutex, +} + +impl<'a, 'b> LeafBuilder<'a, 'b> { + pub(super) fn required_bytes(&self, num_pairs: usize, keys_values_bytes: usize) -> usize { + RawLeafBuilder::required_bytes( + num_pairs, + keys_values_bytes, + self.fixed_key_size, + self.fixed_value_size, + ) + } + + pub(super) fn new( + mem: &'b TransactionalMemory, + allocated_pages: &'b Mutex, + capacity: usize, + fixed_key_size: Option, + fixed_value_size: Option, + ) -> Self { + Self { + pairs: Vec::with_capacity(capacity), + fixed_key_size, + fixed_value_size, + total_key_bytes: 0, + total_value_bytes: 0, + mem, + allocated_pages, + } + } + + pub(super) fn push(&mut self, key: &'a [u8], value: &'a [u8]) { + self.total_key_bytes += key.len(); + self.total_value_bytes += value.len(); + self.pairs.push((key, value)); + } + + pub(super) fn push_all_except( + &mut self, + accessor: &'a LeafAccessor<'_>, + except: Option, + ) { + for i in 0..accessor.num_pairs() { + if let Some(except) = except + && except == i + { + continue; + } + let entry = accessor.entry(i).unwrap(); + self.push(entry.key(), entry.value()); + } + } + + pub(super) fn should_split(&self) -> bool { + let required_size = self.required_bytes( + self.pairs.len(), + self.total_key_bytes + self.total_value_bytes, + ); + required_size > self.mem.get_page_size() && self.pairs.len() > 1 + } + + pub(super) fn build_split<'txn>(self) -> Result<(PageMut<'txn>, &'a [u8], PageMut<'txn>)> { + let total_size = self.total_key_bytes + self.total_value_bytes; + let mut division = 0; + let mut first_split_key_bytes = 0; + let mut first_split_value_bytes = 0; + for (key, value) in self.pairs.iter().take(self.pairs.len() - 1) { + first_split_key_bytes += key.len(); + first_split_value_bytes += value.len(); + division += 1; + if first_split_key_bytes + first_split_value_bytes >= total_size / 2 { + break; + } + } + + let required_size = + self.required_bytes(division, first_split_key_bytes + first_split_value_bytes); + let mut allocated_pages = self.allocated_pages.lock().unwrap(); + let mut page1 = self.mem.allocate(required_size, &mut allocated_pages)?; + let mut builder = RawLeafBuilder::new( + page1.memory_mut(), + division, + self.fixed_key_size, + self.fixed_value_size, + first_split_key_bytes, + ); + for (key, value) in self.pairs.iter().take(division) { + builder.append(key, value); + } + drop(builder); + + let required_size = self.required_bytes( + self.pairs.len() - division, + self.total_key_bytes + self.total_value_bytes + - first_split_key_bytes + - first_split_value_bytes, + ); + let mut page2 = self.mem.allocate(required_size, &mut allocated_pages)?; + let mut builder = RawLeafBuilder::new( + page2.memory_mut(), + self.pairs.len() - division, + self.fixed_key_size, + self.fixed_value_size, + self.total_key_bytes - first_split_key_bytes, + ); + for (key, value) in &self.pairs[division..] { + builder.append(key, value); + } + drop(builder); + + Ok((page1, self.pairs[division - 1].0, page2)) + } + + pub(super) fn build<'txn>(self) -> Result> { + let required_size = self.required_bytes( + self.pairs.len(), + self.total_key_bytes + self.total_value_bytes, + ); + let mut allocated_pages = self.allocated_pages.lock().unwrap(); + let mut page = self.mem.allocate(required_size, &mut allocated_pages)?; + let mut builder = RawLeafBuilder::new( + page.memory_mut(), + self.pairs.len(), + self.fixed_key_size, + self.fixed_value_size, + self.total_key_bytes, + ); + for (key, value) in self.pairs { + builder.append(key, value); + } + drop(builder); + Ok(page) + } +} + +// Note the caller is responsible for ensuring that the buffer is large enough +// and rewriting all fields if any dynamically sized fields are written +// Layout is: +// 1 byte: type +// 1 byte: reserved (padding to 32bits aligned) +// 2 bytes: num_entries (number of pairs) +// (optional) repeating (num_entries times): +// 4 bytes: key_end +// (optional) repeating (num_entries times): +// 4 bytes: value_end +// repeating (num_entries times): +// * n bytes: key data +// repeating (num_entries times): +// * n bytes: value data +pub(crate) struct RawLeafBuilder<'a> { + page: &'a mut [u8], + fixed_key_size: Option, + fixed_value_size: Option, + num_pairs: usize, + provisioned_key_bytes: usize, + pairs_written: usize, // used for debugging +} + +impl<'a> RawLeafBuilder<'a> { + pub(crate) fn required_bytes( + num_pairs: usize, + keys_values_bytes: usize, + key_size: Option, + value_size: Option, + ) -> usize { + // Page id & header; + let mut result = 4; + // key & value lengths + if key_size.is_none() { + result += num_pairs * size_of::(); + } + if value_size.is_none() { + result += num_pairs * size_of::(); + } + result += keys_values_bytes; + + result + } + + pub(crate) fn new( + page: &'a mut [u8], + num_pairs: usize, + fixed_key_size: Option, + fixed_value_size: Option, + key_bytes: usize, + ) -> Self { + page[0] = LEAF; + page[2..4].copy_from_slice(&u16::try_from(num_pairs).unwrap().to_le_bytes()); + #[cfg(debug_assertions)] + { + // Poison all the key & value offsets, in case the caller forgets to write them + let mut last = 4; + if fixed_key_size.is_none() { + last += size_of::() * num_pairs; + } + if fixed_value_size.is_none() { + last += size_of::() * num_pairs; + } + for x in &mut page[4..last] { + *x = 0xFF; + } + } + RawLeafBuilder { + page, + fixed_key_size, + fixed_value_size, + num_pairs, + provisioned_key_bytes: key_bytes, + pairs_written: 0, + } + } + + fn value_end(&self, n: usize) -> usize { + if let Some(fixed) = self.fixed_value_size { + return self.key_section_start() + self.provisioned_key_bytes + fixed * (n + 1); + } + let mut offset = 4 + size_of::() * n; + if self.fixed_key_size.is_none() { + offset += size_of::() * self.num_pairs; + } + u32::from_le_bytes( + self.page[offset..(offset + size_of::())] + .try_into() + .unwrap(), + ) as usize + } + + fn key_section_start(&self) -> usize { + let mut offset = 4; + if self.fixed_key_size.is_none() { + offset += size_of::() * self.num_pairs; + } + if self.fixed_value_size.is_none() { + offset += size_of::() * self.num_pairs; + } + + offset + } + + fn key_end(&self, n: usize) -> usize { + if let Some(fixed) = self.fixed_key_size { + return self.key_section_start() + fixed * (n + 1); + } + let offset = 4 + size_of::() * n; + u32::from_le_bytes( + self.page[offset..(offset + size_of::())] + .try_into() + .unwrap(), + ) as usize + } + + pub(crate) fn append(&mut self, key: &[u8], value: &[u8]) { + if let Some(key_width) = self.fixed_key_size { + assert_eq!(key_width, key.len()); + } + if let Some(value_width) = self.fixed_value_size { + assert_eq!(value_width, value.len()); + } + let key_offset = if self.pairs_written == 0 { + self.key_section_start() + } else { + self.key_end(self.pairs_written - 1) + }; + let value_offset = if self.pairs_written == 0 { + self.key_section_start() + self.provisioned_key_bytes + } else { + self.value_end(self.pairs_written - 1) + }; + + let n = self.pairs_written; + if self.fixed_key_size.is_none() { + let offset = 4 + size_of::() * n; + self.page[offset..(offset + size_of::())] + .copy_from_slice(&u32::try_from(key_offset + key.len()).unwrap().to_le_bytes()); + } + self.page[key_offset..(key_offset + key.len())].copy_from_slice(key); + let written_key_len = key_offset + key.len() - self.key_section_start(); + assert!(written_key_len <= self.provisioned_key_bytes); + + if self.fixed_value_size.is_none() { + let mut offset = 4 + size_of::() * n; + if self.fixed_key_size.is_none() { + offset += size_of::() * self.num_pairs; + } + self.page[offset..(offset + size_of::())].copy_from_slice( + &u32::try_from(value_offset + value.len()) + .unwrap() + .to_le_bytes(), + ); + } + self.page[value_offset..(value_offset + value.len())].copy_from_slice(value); + self.pairs_written += 1; + } +} + +impl Drop for RawLeafBuilder<'_> { + fn drop(&mut self) { + if !thread::panicking() { + assert_eq!(self.pairs_written, self.num_pairs); + assert_eq!( + self.key_section_start() + self.provisioned_key_bytes, + self.key_end(self.num_pairs - 1) + ); + } + } +} + +pub(crate) struct LeafMutator<'b> { + page: &'b mut [u8], + fixed_key_size: Option, + fixed_value_size: Option, +} + +impl<'b> LeafMutator<'b> { + pub(crate) fn new( + page: &'b mut [u8], + fixed_key_size: Option, + fixed_value_size: Option, + ) -> Self { + assert_eq!(page[0], LEAF); + Self { + page, + fixed_key_size, + fixed_value_size, + } + } + + // Returns true if there is enough space to replace the value at `position` in-place. + // The key at `position` is left unchanged. + pub(super) fn sufficient_replace_inplace_space( + page: &'_ impl Page, + position: usize, + fixed_key_size: Option, + fixed_value_size: Option, + new_value: &[u8], + ) -> bool { + let accessor = LeafAccessor::new(page.memory(), fixed_key_size, fixed_value_size); + let remaining = page.memory().len() - accessor.total_length(); + let existing_value_len = accessor + .value_range(position) + .map(|(s, e)| e - s) + .unwrap_or_default(); + let required_delta = isize::try_from(new_value.len()).unwrap() + - isize::try_from(existing_value_len).unwrap(); + required_delta <= isize::try_from(remaining).unwrap() + } + + pub(super) fn sufficient_insert_inplace_space( + page: &'_ impl Page, + position: usize, + fixed_key_size: Option, + fixed_value_size: Option, + new_key: &[u8], + new_value: &[u8], + ) -> bool { + let accessor = LeafAccessor::new(page.memory(), fixed_key_size, fixed_value_size); + // If this is a large page, only allow in-place appending to avoid write amplification + // + // Note: this check is also required to avoid inserting an unbounded number of small values + // into a large page, which could result in overflowing a u16 which is the maximum number of entries per leaf + if page.get_page_number().page_order > 0 && position < accessor.num_pairs() { + return false; + } + let remaining = page.memory().len() - accessor.total_length(); + let mut required_delta = new_key.len() + new_value.len(); + if fixed_key_size.is_none() { + required_delta += size_of::(); + } + if fixed_value_size.is_none() { + required_delta += size_of::(); + } + required_delta <= remaining + } + + // Replace the value at index `i` with `value`, leaving the key unchanged. + pub(crate) fn replace(&mut self, i: usize, value: &[u8]) { + let accessor = LeafAccessor::new(self.page, self.fixed_key_size, self.fixed_value_size); + let num_pairs = accessor.num_pairs(); + let last_value_end = accessor.value_end(num_pairs - 1).unwrap(); + let shift_value_start = accessor.value_start(i + 1).unwrap_or(last_value_end); + let existing_value_len = accessor + .value_range(i) + .map(|(start, end)| end - start) + .unwrap_or_default(); + + let value_delta = + isize::try_from(value.len()).unwrap() - isize::try_from(existing_value_len).unwrap(); + assert!( + isize::try_from(accessor.total_length()).unwrap() + value_delta + <= isize::try_from(self.page.len()).unwrap() + ); + + // Update value end pointers for i..num_pairs + for j in i..num_pairs { + self.update_value_end(j, value_delta); + } + + // Shift trailing values to accommodate the new value size + let mut dest: usize = (isize::try_from(shift_value_start).unwrap() + value_delta) + .try_into() + .unwrap(); + self.page + .copy_within(shift_value_start..last_value_end, dest); + + // Write the new value + dest -= value.len(); + self.page[dest..(dest + value.len())].copy_from_slice(value); + } + + // Insert the given key, value pair at index i and shift all following pairs to the right + pub(crate) fn insert(&mut self, i: usize, key: &[u8], value: &[u8]) { + let accessor = LeafAccessor::new(self.page, self.fixed_key_size, self.fixed_value_size); + let required_delta = { + let mut delta = key.len() + value.len(); + if self.fixed_key_size.is_none() { + delta += size_of::(); + } + if self.fixed_value_size.is_none() { + delta += size_of::(); + } + isize::try_from(delta).unwrap() + }; + assert!( + isize::try_from(accessor.total_length()).unwrap() + required_delta + <= isize::try_from(self.page.len()).unwrap() + ); + + let num_pairs = accessor.num_pairs(); + let last_key_end = accessor.key_end(accessor.num_pairs() - 1).unwrap(); + let last_value_end = accessor.value_end(accessor.num_pairs() - 1).unwrap(); + let shift_key_start = accessor.key_start(i).unwrap_or(last_key_end); + let shift_value_start = accessor.value_start(i).unwrap_or(last_value_end); + + // Update all the pointers + let key_ptr_size: usize = if self.fixed_key_size.is_none() { 4 } else { 0 }; + let value_ptr_size: usize = if self.fixed_value_size.is_none() { + 4 + } else { + 0 + }; + for j in 0..i { + self.update_key_end(j, (key_ptr_size + value_ptr_size).try_into().unwrap()); + let value_delta: isize = (key_ptr_size + value_ptr_size + key.len()) + .try_into() + .unwrap(); + self.update_value_end(j, value_delta); + } + for j in i..num_pairs { + let key_delta: isize = (key_ptr_size + value_ptr_size + key.len()) + .try_into() + .unwrap(); + self.update_key_end(j, key_delta); + let value_delta = key_delta + isize::try_from(value.len()).unwrap(); + self.update_value_end(j, value_delta); + } + + let new_num_pairs = num_pairs + 1; + self.page[2..4].copy_from_slice(&u16::try_from(new_num_pairs).unwrap().to_le_bytes()); + + // Right shift the trailing values + let mut dest = shift_value_start + key_ptr_size + value_ptr_size + key.len() + value.len(); + let start = shift_value_start; + let end = last_value_end; + self.page.copy_within(start..end, dest); + + // Insert the value + let inserted_value_end: u32 = dest.try_into().unwrap(); + dest -= value.len(); + self.page[dest..(dest + value.len())].copy_from_slice(value); + + // Right shift the trailing key data & preceding value data + let start = shift_key_start; + let end = shift_value_start; + dest -= end - start; + self.page.copy_within(start..end, dest); + + // Insert the key + let inserted_key_end: u32 = dest.try_into().unwrap(); + dest -= key.len(); + self.page[dest..(dest + key.len())].copy_from_slice(key); + + // Right shift the trailing value pointers & preceding key data + let start = 4 + key_ptr_size * num_pairs + value_ptr_size * i; + let end = shift_key_start; + dest -= end - start; + debug_assert_eq!( + dest, + 4 + key_ptr_size * new_num_pairs + value_ptr_size * (i + 1) + ); + self.page.copy_within(start..end, dest); + + // Insert the value pointer + if self.fixed_value_size.is_none() { + dest -= size_of::(); + self.page[dest..(dest + size_of::())] + .copy_from_slice(&inserted_value_end.to_le_bytes()); + } + + // Right shift the trailing key pointers & preceding value pointers + let start = 4 + key_ptr_size * i; + let end = 4 + key_ptr_size * num_pairs + value_ptr_size * i; + dest -= end - start; + debug_assert_eq!(dest, 4 + key_ptr_size * (i + 1)); + self.page.copy_within(start..end, dest); + + // Insert the key pointer + if self.fixed_key_size.is_none() { + dest -= size_of::(); + self.page[dest..(dest + size_of::())] + .copy_from_slice(&inserted_key_end.to_le_bytes()); + } + debug_assert_eq!(dest, 4 + key_ptr_size * i); + } + + pub(super) fn remove(&mut self, i: usize) { + let accessor = LeafAccessor::new(self.page, self.fixed_key_size, self.fixed_value_size); + let num_pairs = accessor.num_pairs(); + assert!(i < num_pairs); + assert!(num_pairs > 1); + let key_start = accessor.key_start(i).unwrap(); + let key_end = accessor.key_end(i).unwrap(); + let value_start = accessor.value_start(i).unwrap(); + let value_end = accessor.value_end(i).unwrap(); + let last_value_end = accessor.value_end(accessor.num_pairs() - 1).unwrap(); + + // Update all the pointers + let key_ptr_size = if self.fixed_key_size.is_none() { + size_of::() + } else { + 0 + }; + let value_ptr_size = if self.fixed_value_size.is_none() { + size_of::() + } else { + 0 + }; + for j in 0..i { + self.update_key_end(j, -isize::try_from(key_ptr_size + value_ptr_size).unwrap()); + let value_delta = -isize::try_from(key_ptr_size + value_ptr_size).unwrap() + - isize::try_from(key_end - key_start).unwrap(); + self.update_value_end(j, value_delta); + } + for j in (i + 1)..num_pairs { + let key_delta = -isize::try_from(key_ptr_size + value_ptr_size).unwrap() + - isize::try_from(key_end - key_start).unwrap(); + self.update_key_end(j, key_delta); + let value_delta = key_delta - isize::try_from(value_end - value_start).unwrap(); + self.update_value_end(j, value_delta); + } + + // Left shift all the pointers & data + + let new_num_pairs = num_pairs - 1; + self.page[2..4].copy_from_slice(&u16::try_from(new_num_pairs).unwrap().to_le_bytes()); + // Left shift the trailing key pointers & preceding value pointers + let mut dest = 4 + key_ptr_size * i; + // First trailing key pointer + let start = 4 + key_ptr_size * (i + 1); + // Last preceding value pointer + let end = 4 + key_ptr_size * num_pairs + value_ptr_size * i; + self.page.copy_within(start..end, dest); + dest += end - start; + debug_assert_eq!(dest, 4 + key_ptr_size * new_num_pairs + value_ptr_size * i); + + // Left shift the trailing value pointers & preceding key data + let start = 4 + key_ptr_size * num_pairs + value_ptr_size * (i + 1); + let end = key_start; + self.page.copy_within(start..end, dest); + dest += end - start; + + let preceding_key_len = key_start - (4 + (key_ptr_size + value_ptr_size) * num_pairs); + debug_assert_eq!( + dest, + 4 + (key_ptr_size + value_ptr_size) * new_num_pairs + preceding_key_len + ); + + // Left shift the trailing key data & preceding value data + let start = key_end; + let end = value_start; + self.page.copy_within(start..end, dest); + dest += end - start; + + // Left shift the trailing value data + let preceding_data_len = + value_start - (4 + (key_ptr_size + value_ptr_size) * num_pairs) - (key_end - key_start); + debug_assert_eq!( + dest, + 4 + (key_ptr_size + value_ptr_size) * new_num_pairs + preceding_data_len + ); + let start = value_end; + let end = last_value_end; + self.page.copy_within(start..end, dest); + } + + fn update_key_end(&mut self, i: usize, delta: isize) { + if self.fixed_key_size.is_some() { + return; + } + let offset = 4 + size_of::() * i; + let mut ptr = u32::from_le_bytes( + self.page[offset..(offset + size_of::())] + .try_into() + .unwrap(), + ); + ptr = (isize::try_from(ptr).unwrap() + delta).try_into().unwrap(); + self.page[offset..(offset + size_of::())].copy_from_slice(&ptr.to_le_bytes()); + } + + fn update_value_end(&mut self, i: usize, delta: isize) { + if self.fixed_value_size.is_some() { + return; + } + let accessor = LeafAccessor::new(self.page, self.fixed_key_size, self.fixed_value_size); + let num_pairs = accessor.num_pairs(); + let mut offset = 4 + size_of::() * i; + if self.fixed_key_size.is_none() { + offset += size_of::() * num_pairs; + } + let mut ptr = u32::from_le_bytes( + self.page[offset..(offset + size_of::())] + .try_into() + .unwrap(), + ); + ptr = (isize::try_from(ptr).unwrap() + delta).try_into().unwrap(); + self.page[offset..(offset + size_of::())].copy_from_slice(&ptr.to_le_bytes()); + } +} + +// Provides a simple zero-copy way to access a branch page +// TODO: this should be pub(super) and the multimap btree stuff should be moved into this package +pub(crate) struct BranchAccessor<'a: 'b, 'b, T: Page + 'a> { + page: &'b T, + num_keys: usize, + fixed_key_size: Option, + _page_lifetime: PhantomData<&'a ()>, +} + +impl<'a: 'b, 'b, T: Page + 'a> BranchAccessor<'a, 'b, T> { + pub(crate) fn new(page: &'b T, fixed_key_size: Option) -> Self { + debug_assert_eq!(page.memory()[0], BRANCH); + let num_keys = u16::from_le_bytes(page.memory()[2..4].try_into().unwrap()) as usize; + BranchAccessor { + page, + num_keys, + fixed_key_size, + _page_lifetime: Default::default(), + } + } + + pub(super) fn print_node(&self) { + eprint!( + "Internal[ (page={:?}), child_0={:?}", + self.page.get_page_number(), + self.child_page(0).unwrap() + ); + for i in 0..(self.count_children() - 1) { + if let Some(child) = self.child_page(i + 1) { + let key = self.key(i).unwrap(); + eprint!(" key_{i}={:?}", K::from_bytes(key)); + eprint!(" child_{}={child:?}", i + 1); + } + } + eprint!("]"); + } + + pub(crate) fn total_length(&self) -> usize { + // Keys are stored at the end + self.key_end(self.num_keys() - 1).unwrap() + } + + pub(super) fn child_for_key(&self, query: &[u8]) -> (usize, PageNumber) { + let mut min_child = 0; // inclusive + let mut max_child = self.num_keys(); // inclusive + while min_child < max_child { + let mid = min_child.midpoint(max_child); + match K::compare(query, self.key(mid).unwrap()) { + Ordering::Less => { + max_child = mid; + } + Ordering::Equal => { + return (mid, self.child_page(mid).unwrap()); + } + Ordering::Greater => { + min_child = mid + 1; + } + } + } + debug_assert_eq!(min_child, max_child); + + (min_child, self.child_page(min_child).unwrap()) + } + + fn key_section_start(&self) -> usize { + if self.fixed_key_size.is_none() { + 8 + (PageNumber::serialized_size() + size_of::()) * self.count_children() + + size_of::() * self.num_keys() + } else { + 8 + (PageNumber::serialized_size() + size_of::()) * self.count_children() + } + } + + fn key_offset(&self, n: usize) -> usize { + if n == 0 { + self.key_section_start() + } else { + self.key_end(n - 1).unwrap() + } + } + + fn key_end(&self, n: usize) -> Option { + if let Some(fixed) = self.fixed_key_size { + return Some(self.key_section_start() + fixed * (n + 1)); + } + let offset = 8 + + (PageNumber::serialized_size() + size_of::()) * self.count_children() + + size_of::() * n; + Some(u32::from_le_bytes( + self.page + .memory() + .get(offset..(offset + size_of::()))? + .try_into() + .unwrap(), + ) as usize) + } + + pub(super) fn key(&self, n: usize) -> Option<&[u8]> { + if n >= self.num_keys() { + return None; + } + let offset = self.key_offset(n); + let end = self.key_end(n)?; + Some(&self.page.memory()[offset..end]) + } + + pub(crate) fn count_children(&self) -> usize { + self.num_keys() + 1 + } + + pub(crate) fn child_checksum(&self, n: usize) -> Option { + if n >= self.count_children() { + return None; + } + + let offset = 8 + size_of::() * n; + Some(Checksum::from_le_bytes( + self.page.memory()[offset..(offset + size_of::())] + .try_into() + .unwrap(), + )) + } + + pub(crate) fn child_page(&self, n: usize) -> Option { + if n >= self.count_children() { + return None; + } + + let offset = + 8 + size_of::() * self.count_children() + PageNumber::serialized_size() * n; + Some(PageNumber::from_le_bytes( + self.page.memory()[offset..(offset + PageNumber::serialized_size())] + .try_into() + .unwrap(), + )) + } + + fn num_keys(&self) -> usize { + self.num_keys + } +} + +pub(super) struct BranchBuilder<'a, 'b> { + children: Vec<(PageNumber, Checksum)>, + keys: Vec<&'a [u8]>, + total_key_bytes: usize, + fixed_key_size: Option, + mem: &'b TransactionalMemory, + allocated_pages: &'b Mutex, +} + +impl<'a, 'b> BranchBuilder<'a, 'b> { + pub(super) fn new( + mem: &'b TransactionalMemory, + allocated_pages: &'b Mutex, + child_capacity: usize, + fixed_key_size: Option, + ) -> Self { + Self { + children: Vec::with_capacity(child_capacity), + keys: Vec::with_capacity(child_capacity - 1), + total_key_bytes: 0, + fixed_key_size, + mem, + allocated_pages, + } + } + + pub(super) fn replace_child(&mut self, index: usize, child: PageNumber, checksum: Checksum) { + self.children[index] = (child, checksum); + } + + pub(super) fn push_child(&mut self, child: PageNumber, checksum: Checksum) { + self.children.push((child, checksum)); + } + + pub(super) fn push_key(&mut self, key: &'a [u8]) { + self.keys.push(key); + self.total_key_bytes += key.len(); + } + + pub(super) fn push_all(&mut self, accessor: &'a BranchAccessor<'_, '_, T>) { + for i in 0..accessor.count_children() { + let child = accessor.child_page(i).unwrap(); + let checksum = accessor.child_checksum(i).unwrap(); + self.push_child(child, checksum); + } + for i in 0..(accessor.count_children() - 1) { + self.push_key(accessor.key(i).unwrap()); + } + } + + pub(super) fn to_single_child(&self) -> Option<(PageNumber, Checksum)> { + if self.children.len() > 1 { + None + } else { + Some(self.children[0]) + } + } + + pub(super) fn required_bytes(&self) -> usize { + RawBranchBuilder::required_bytes(self.keys.len(), self.total_key_bytes, self.fixed_key_size) + } + + pub(super) fn into_parts(self) -> (Vec<(PageNumber, Checksum)>, Vec>) { + let owned_keys = self.keys.into_iter().map(<[u8]>::to_vec).collect(); + (self.children, owned_keys) + } + + pub(super) fn build<'txn>(self) -> Result> { + assert_eq!(self.children.len(), self.keys.len() + 1); + let size = RawBranchBuilder::required_bytes( + self.keys.len(), + self.total_key_bytes, + self.fixed_key_size, + ); + let mut allocated_pages = self.allocated_pages.lock().unwrap(); + let mut page = self.mem.allocate(size, &mut allocated_pages)?; + let mut builder = + RawBranchBuilder::new(page.memory_mut(), self.keys.len(), self.fixed_key_size); + builder.write_first_page(self.children[0].0, self.children[0].1); + for i in 1..self.children.len() { + let key = &self.keys[i - 1]; + builder.write_nth_key(key.as_ref(), self.children[i].0, self.children[i].1, i - 1); + } + drop(builder); + + Ok(page) + } + + pub(super) fn should_split(&self) -> bool { + let size = RawBranchBuilder::required_bytes( + self.keys.len(), + self.total_key_bytes, + self.fixed_key_size, + ); + size > self.mem.get_page_size() && self.keys.len() >= 3 + } + + pub(super) fn build_split<'txn>(self) -> Result<(PageMut<'txn>, &'a [u8], PageMut<'txn>)> { + let mut allocated_pages = self.allocated_pages.lock().unwrap(); + assert_eq!(self.children.len(), self.keys.len() + 1); + assert!(self.keys.len() >= 3); + let division = self.keys.len() / 2; + let first_split_key_len: usize = self.keys.iter().take(division).map(|k| k.len()).sum(); + let division_key = self.keys[division]; + let second_split_key_len = self.total_key_bytes - first_split_key_len - division_key.len(); + + let size = + RawBranchBuilder::required_bytes(division, first_split_key_len, self.fixed_key_size); + let mut page1 = self.mem.allocate(size, &mut allocated_pages)?; + let mut builder = RawBranchBuilder::new(page1.memory_mut(), division, self.fixed_key_size); + builder.write_first_page(self.children[0].0, self.children[0].1); + for i in 0..division { + let key = &self.keys[i]; + builder.write_nth_key( + key.as_ref(), + self.children[i + 1].0, + self.children[i + 1].1, + i, + ); + } + drop(builder); + + let size = RawBranchBuilder::required_bytes( + self.keys.len() - division - 1, + second_split_key_len, + self.fixed_key_size, + ); + let mut page2 = self.mem.allocate(size, &mut allocated_pages)?; + let mut builder = RawBranchBuilder::new( + page2.memory_mut(), + self.keys.len() - division - 1, + self.fixed_key_size, + ); + builder.write_first_page(self.children[division + 1].0, self.children[division + 1].1); + for i in (division + 1)..self.keys.len() { + let key = &self.keys[i]; + builder.write_nth_key( + key.as_ref(), + self.children[i + 1].0, + self.children[i + 1].1, + i - division - 1, + ); + } + drop(builder); + + Ok((page1, division_key, page2)) + } +} + +// Note the caller is responsible for ensuring that the buffer is large enough +// and rewriting all fields if any dynamically sized fields are written +// Layout is: +// 1 byte: type +// 1 byte: padding (padding to 16bits aligned) +// 2 bytes: num_keys (number of keys) +// 4 byte: padding (padding to 64bits aligned) +// repeating (num_keys + 1 times): +// 16 bytes: child page checksum +// repeating (num_keys + 1 times): +// 8 bytes: page number +// (optional) repeating (num_keys times): +// * 4 bytes: key end. Ending offset of the key, exclusive +// repeating (num_keys times): +// * n bytes: key data +pub(super) struct RawBranchBuilder<'b> { + page: &'b mut [u8], + fixed_key_size: Option, + num_keys: usize, + keys_written: usize, // used for debugging +} + +impl<'b> RawBranchBuilder<'b> { + pub(super) fn required_bytes( + num_keys: usize, + size_of_keys: usize, + fixed_key_size: Option, + ) -> usize { + if fixed_key_size.is_none() { + let fixed_size = 8 + + (PageNumber::serialized_size() + size_of::()) * (num_keys + 1) + + size_of::() * num_keys; + size_of_keys + fixed_size + } else { + let fixed_size = + 8 + (PageNumber::serialized_size() + size_of::()) * (num_keys + 1); + size_of_keys + fixed_size + } + } + + // Caller MUST write num_keys values + pub(super) fn new(page: &'b mut [u8], num_keys: usize, fixed_key_size: Option) -> Self { + assert!(num_keys > 0); + page[0] = BRANCH; + page[2..4].copy_from_slice(&u16::try_from(num_keys).unwrap().to_le_bytes()); + #[cfg(debug_assertions)] + { + // Poison all the child pointers & key offsets, in case the caller forgets to write them + let start = 8 + size_of::() * (num_keys + 1); + let mut last = + 8 + (PageNumber::serialized_size() + size_of::()) * (num_keys + 1); + if fixed_key_size.is_none() { + last += size_of::() * num_keys; + } + for x in &mut page[start..last] { + *x = 0xFF; + } + } + RawBranchBuilder { + page, + fixed_key_size, + num_keys, + keys_written: 0, + } + } + + pub(super) fn write_first_page(&mut self, page_number: PageNumber, checksum: Checksum) { + let offset = 8; + self.page[offset..(offset + size_of::())] + .copy_from_slice(&checksum.to_le_bytes()); + let offset = 8 + size_of::() * (self.num_keys + 1); + self.page[offset..(offset + PageNumber::serialized_size())] + .copy_from_slice(&page_number.to_le_bytes()); + } + + fn key_section_start(&self) -> usize { + let mut offset = + 8 + (PageNumber::serialized_size() + size_of::()) * (self.num_keys + 1); + if self.fixed_key_size.is_none() { + offset += size_of::() * self.num_keys; + } + + offset + } + + fn key_end(&self, n: usize) -> usize { + if let Some(fixed) = self.fixed_key_size { + return self.key_section_start() + fixed * (n + 1); + } + let offset = 8 + + (PageNumber::serialized_size() + size_of::()) * (self.num_keys + 1) + + size_of::() * n; + u32::from_le_bytes( + self.page[offset..(offset + size_of::())] + .try_into() + .unwrap(), + ) as usize + } + + // Write the nth key and page of values greater than this key, but less than or equal to the next + // Caller must write keys & pages in increasing order + pub(super) fn write_nth_key( + &mut self, + key: &[u8], + page_number: PageNumber, + checksum: Checksum, + n: usize, + ) { + assert!(n < self.num_keys); + assert_eq!(n, self.keys_written); + self.keys_written += 1; + let offset = 8 + size_of::() * (n + 1); + self.page[offset..(offset + size_of::())] + .copy_from_slice(&checksum.to_le_bytes()); + let offset = 8 + + size_of::() * (self.num_keys + 1) + + PageNumber::serialized_size() * (n + 1); + self.page[offset..(offset + PageNumber::serialized_size())] + .copy_from_slice(&page_number.to_le_bytes()); + + let data_offset = if n > 0 { + self.key_end(n - 1) + } else { + self.key_section_start() + }; + if self.fixed_key_size.is_none() { + let offset = 8 + + (PageNumber::serialized_size() + size_of::()) * (self.num_keys + 1) + + size_of::() * n; + self.page[offset..(offset + size_of::())].copy_from_slice( + &u32::try_from(data_offset + key.len()) + .unwrap() + .to_le_bytes(), + ); + } + + debug_assert!(data_offset > offset); + self.page[data_offset..(data_offset + key.len())].copy_from_slice(key); + } +} + +impl Drop for RawBranchBuilder<'_> { + fn drop(&mut self) { + if !thread::panicking() { + assert_eq!(self.keys_written, self.num_keys); + } + } +} + +pub(crate) struct BranchMutator<'b> { + page: &'b mut [u8], +} + +impl<'b> BranchMutator<'b> { + pub(crate) fn new(page: &'b mut [u8]) -> Self { + assert_eq!(page[0], BRANCH); + Self { page } + } + + fn num_keys(&self) -> usize { + u16::from_le_bytes(self.page[2..4].try_into().unwrap()) as usize + } + + pub(crate) fn write_child_page( + &mut self, + i: usize, + page_number: PageNumber, + checksum: Checksum, + ) { + debug_assert!(i <= self.num_keys()); + let offset = 8 + size_of::() * i; + self.page[offset..(offset + size_of::())] + .copy_from_slice(&checksum.to_le_bytes()); + let offset = + 8 + size_of::() * (self.num_keys() + 1) + PageNumber::serialized_size() * i; + self.page[offset..(offset + PageNumber::serialized_size())] + .copy_from_slice(&page_number.to_le_bytes()); + } +} diff --git a/v2/redb/src/tree_store/btree_iters.rs b/v2/redb/src/tree_store/btree_iters.rs new file mode 100644 index 000000000..e6841df38 --- /dev/null +++ b/v2/redb/src/tree_store/btree_iters.rs @@ -0,0 +1,811 @@ +use crate::Result; +use crate::tree_store::btree_base::{BRANCH, LEAF}; +use crate::tree_store::btree_base::{BranchAccessor, LeafAccessor}; +use crate::tree_store::btree_iters::RangeIterState::{Internal, Leaf}; +use crate::tree_store::btree_mutator::MutateHelper; +use crate::tree_store::page_store::{Page, PageHint, PageImpl, TransactionalMemory}; +use crate::tree_store::{BtreeHeader, PageNumber, PageTrackerPolicy}; +use crate::types::{Key, Value}; +use Bound::{Excluded, Included, Unbounded}; +use std::borrow::Borrow; +use std::collections::Bound; +use std::marker::PhantomData; +use std::ops::{Range, RangeBounds}; +use std::sync::{Arc, Mutex}; + +#[derive(Debug, Clone)] +pub enum RangeIterState { + Leaf { + page: PageImpl, + fixed_key_size: Option, + fixed_value_size: Option, + entry: usize, + parent: Option>, + }, + Internal { + page: PageImpl, + fixed_key_size: Option, + fixed_value_size: Option, + child: usize, + parent: Option>, + }, +} + +impl RangeIterState { + fn page_number(&self) -> PageNumber { + match self { + Leaf { page, .. } | Internal { page, .. } => page.get_page_number(), + } + } + + fn next( + self, + reverse: bool, + manager: &TransactionalMemory, + hint: PageHint, + ) -> Result> { + match self { + Leaf { + page, + fixed_key_size, + fixed_value_size, + entry, + parent, + } => { + let accessor = LeafAccessor::new(page.memory(), fixed_key_size, fixed_value_size); + let direction = if reverse { -1 } else { 1 }; + let next_entry = isize::try_from(entry).unwrap() + direction; + if 0 <= next_entry && next_entry < accessor.num_pairs().try_into().unwrap() { + Ok(Some(Leaf { + page, + fixed_key_size, + fixed_value_size, + entry: next_entry.try_into().unwrap(), + parent, + })) + } else { + Ok(parent.map(|x| *x)) + } + } + Internal { + page, + fixed_key_size, + fixed_value_size, + child, + mut parent, + } => { + let accessor = BranchAccessor::new(&page, fixed_key_size); + let child_page = accessor.child_page(child).unwrap(); + let child_page = manager.get_page(child_page, hint)?; + let direction = if reverse { -1 } else { 1 }; + let next_child = isize::try_from(child).unwrap() + direction; + if 0 <= next_child && next_child < accessor.count_children().try_into().unwrap() { + parent = Some(Box::new(Internal { + page, + fixed_key_size, + fixed_value_size, + child: next_child.try_into().unwrap(), + parent, + })); + } + match child_page.memory()[0] { + LEAF => { + let child_accessor = LeafAccessor::new( + child_page.memory(), + fixed_key_size, + fixed_value_size, + ); + let entry = if reverse { + child_accessor.num_pairs() - 1 + } else { + 0 + }; + Ok(Some(Leaf { + page: child_page, + fixed_key_size, + fixed_value_size, + entry, + parent, + })) + } + BRANCH => { + let child_accessor = BranchAccessor::new(&child_page, fixed_key_size); + let child = if reverse { + child_accessor.count_children() - 1 + } else { + 0 + }; + Ok(Some(Internal { + page: child_page, + fixed_key_size, + fixed_value_size, + child, + parent, + })) + } + _ => unreachable!(), + } + } + } + } + + fn get_entry(&self) -> Option> { + match self { + Leaf { + page, + fixed_key_size, + fixed_value_size, + entry, + .. + } => { + let (key, value) = + LeafAccessor::new(page.memory(), *fixed_key_size, *fixed_value_size) + .entry_ranges(*entry)?; + Some(EntryGuard::new(page.clone(), key, value)) + } + Internal { .. } => None, + } + } +} + +pub(crate) struct EntryGuard { + page: PageImpl, + key_range: Range, + value_range: Range, + _key_type: PhantomData, + _value_type: PhantomData, +} + +impl EntryGuard { + fn new(page: PageImpl, key_range: Range, value_range: Range) -> Self { + Self { + page, + key_range, + value_range, + _key_type: Default::default(), + _value_type: Default::default(), + } + } + + pub(crate) fn key_data(&self) -> Vec { + self.page.memory()[self.key_range.clone()].to_vec() + } + + pub(crate) fn key(&self) -> K::SelfType<'_> { + K::from_bytes(&self.page.memory()[self.key_range.clone()]) + } + + pub(crate) fn value(&self) -> V::SelfType<'_> { + V::from_bytes(&self.page.memory()[self.value_range.clone()]) + } + + pub(crate) fn into_raw(self) -> (PageImpl, Range, Range) { + (self.page, self.key_range, self.value_range) + } +} + +pub(crate) struct AllPageNumbersBtreeIter { + next: Option, + manager: Arc, + hint: PageHint, +} + +impl AllPageNumbersBtreeIter { + pub(crate) fn new( + root: PageNumber, + fixed_key_size: Option, + fixed_value_size: Option, + manager: Arc, + hint: PageHint, + ) -> Result { + let root_page = manager.get_page(root, hint)?; + let node_mem = root_page.memory(); + let start = match node_mem[0] { + LEAF => Leaf { + page: root_page, + fixed_key_size, + fixed_value_size, + entry: 0, + parent: None, + }, + BRANCH => Internal { + page: root_page, + fixed_key_size, + fixed_value_size, + child: 0, + parent: None, + }, + _ => unreachable!(), + }; + Ok(Self { + next: Some(start), + manager, + hint, + }) + } +} + +impl Iterator for AllPageNumbersBtreeIter { + type Item = Result; + + fn next(&mut self) -> Option { + loop { + let state = self.next.take()?; + let value = state.page_number(); + // Only return each page number once + let once = match state { + Leaf { entry, .. } => entry == 0, + Internal { child, .. } => child == 0, + }; + match state.next(false, &self.manager, self.hint) { + Ok(next) => { + self.next = next; + } + Err(err) => { + return Some(Err(err)); + } + } + if once { + return Some(Ok(value)); + } + } + } +} + +pub(crate) struct BtreeExtractIf< + 'a, + K: Key + 'static, + V: Value + 'static, + F: for<'f> FnMut(K::SelfType<'f>, V::SelfType<'f>) -> bool, +> { + root: &'a mut Option, + inner: BtreeRangeIter, + predicate: F, + free_on_drop: Vec, + master_free_list: Arc>>, + allocated: Arc>, + mem: Arc, +} + +impl<'a, K: Key, V: Value, F: for<'f> FnMut(K::SelfType<'f>, V::SelfType<'f>) -> bool> + BtreeExtractIf<'a, K, V, F> +{ + pub(crate) fn new( + root: &'a mut Option, + inner: BtreeRangeIter, + predicate: F, + master_free_list: Arc>>, + allocated: Arc>, + mem: Arc, + ) -> Self { + Self { + root, + inner, + predicate, + free_on_drop: vec![], + master_free_list, + allocated, + mem, + } + } +} + +impl FnMut(K::SelfType<'f>, V::SelfType<'f>) -> bool> Iterator + for BtreeExtractIf<'_, K, V, F> +{ + type Item = Result>; + + fn next(&mut self) -> Option { + let mut item = self.inner.next(); + while let Some(Ok(ref entry)) = item { + if (self.predicate)(entry.key(), entry.value()) { + let mut operation: MutateHelper<'_, '_, K, V> = MutateHelper::new_do_not_modify( + self.root, + self.mem.clone(), + &mut self.free_on_drop, + self.allocated.clone(), + ); + match operation.delete(&entry.key()) { + Ok(x) => { + assert!(x.is_some()); + } + Err(x) => { + return Some(Err(x)); + } + } + break; + } + item = self.inner.next(); + } + item + } +} + +impl FnMut(K::SelfType<'f>, V::SelfType<'f>) -> bool> + DoubleEndedIterator for BtreeExtractIf<'_, K, V, F> +{ + fn next_back(&mut self) -> Option { + let mut item = self.inner.next_back(); + while let Some(Ok(ref entry)) = item { + if (self.predicate)(entry.key(), entry.value()) { + let mut operation: MutateHelper<'_, '_, K, V> = MutateHelper::new_do_not_modify( + self.root, + self.mem.clone(), + &mut self.free_on_drop, + self.allocated.clone(), + ); + match operation.delete(&entry.key()) { + Ok(x) => { + assert!(x.is_some()); + } + Err(x) => { + return Some(Err(x)); + } + } + break; + } + item = self.inner.next_back(); + } + item + } +} + +impl FnMut(K::SelfType<'f>, V::SelfType<'f>) -> bool> Drop + for BtreeExtractIf<'_, K, V, F> +{ + fn drop(&mut self) { + self.inner.close(); + let mut master_free_list = self.master_free_list.lock().unwrap(); + let mut allocated = self.allocated.lock().unwrap(); + for page in self.free_on_drop.drain(..) { + if !self.mem.free_if_uncommitted(page, &mut allocated) { + master_free_list.push(page); + } + } + } +} + +#[derive(Clone)] +pub(crate) struct BtreeRangeIter { + left: Option, // Exclusive. The previous element returned + right: Option, // Exclusive. The previous element returned + include_left: bool, // left is inclusive, instead of exclusive + include_right: bool, // right is inclusive, instead of exclusive + manager: Arc, + hint: PageHint, + // When Some, the right boundary is Unbounded and not yet computed. + // Stores the tree root for lazy initialization on first next_back() call. + uninit_right_root: Option, + _key_type: PhantomData, + _value_type: PhantomData, +} + +fn range_is_empty<'a, K: Key + 'static, KR: Borrow>, T: RangeBounds>( + range: &T, +) -> bool { + match (range.start_bound(), range.end_bound()) { + (Unbounded, _) | (_, Unbounded) => false, + (Included(start), Excluded(end)) | (Excluded(start), Included(end) | Excluded(end)) => { + let start_tmp = K::as_bytes(start.borrow()); + let start_value = start_tmp.as_ref(); + let end_tmp = K::as_bytes(end.borrow()); + let end_value = end_tmp.as_ref(); + K::compare(start_value, end_value).is_ge() + } + (Included(start), Included(end)) => { + let start_tmp = K::as_bytes(start.borrow()); + let start_value = start_tmp.as_ref(); + let end_tmp = K::as_bytes(end.borrow()); + let end_value = end_tmp.as_ref(); + K::compare(start_value, end_value).is_gt() + } + } +} + +impl BtreeRangeIter { + pub(crate) fn new<'a, T: RangeBounds, KR: Borrow>>( + query_range: &'_ T, + table_root: Option, + manager: Arc, + hint: PageHint, + ) -> Result { + if range_is_empty::(query_range) { + return Ok(Self { + left: None, + right: None, + include_left: false, + include_right: false, + manager, + hint, + uninit_right_root: None, + _key_type: Default::default(), + _value_type: Default::default(), + }); + } + if let Some(root) = table_root { + let (include_left, left) = match query_range.start_bound() { + Included(k) => find_iter_left::( + manager.get_page(root, hint)?, + None, + K::as_bytes(k.borrow()).as_ref(), + true, + &manager, + hint, + )?, + Excluded(k) => find_iter_left::( + manager.get_page(root, hint)?, + None, + K::as_bytes(k.borrow()).as_ref(), + false, + &manager, + hint, + )?, + Unbounded => { + let state = find_iter_unbounded::( + manager.get_page(root, hint)?, + None, + false, + &manager, + hint, + )?; + (true, state) + } + }; + // For an unbounded right end, skip the expensive tree traversal to the rightmost + // leaf. The right boundary will be lazily computed on the first next_back() call. + // For forward iteration (next()), right=None correctly means "no upper bound". + let (include_right, right, uninit_right_root) = match query_range.end_bound() { + Included(k) => { + let (inc, state) = find_iter_right::( + manager.get_page(root, hint)?, + None, + K::as_bytes(k.borrow()).as_ref(), + true, + &manager, + hint, + )?; + (inc, state, None) + } + Excluded(k) => { + let (inc, state) = find_iter_right::( + manager.get_page(root, hint)?, + None, + K::as_bytes(k.borrow()).as_ref(), + false, + &manager, + hint, + )?; + (inc, state, None) + } + Unbounded => (true, None, Some(root)), + }; + Ok(Self { + left, + right, + include_left, + include_right, + manager, + hint, + uninit_right_root, + _key_type: Default::default(), + _value_type: Default::default(), + }) + } else { + Ok(Self { + left: None, + right: None, + include_left: false, + include_right: false, + manager, + hint, + uninit_right_root: None, + _key_type: Default::default(), + _value_type: Default::default(), + }) + } + } + + fn close(&mut self) { + self.left = None; + self.right = None; + self.uninit_right_root = None; + } +} + +impl Iterator for BtreeRangeIter { + type Item = Result>; + + fn next(&mut self) -> Option { + if let ( + Some(Leaf { + page: left_page, + entry: left_entry, + .. + }), + Some(Leaf { + page: right_page, + entry: right_entry, + .. + }), + ) = (&self.left, &self.right) + && left_page.get_page_number() == right_page.get_page_number() + && (left_entry > right_entry + || (left_entry == right_entry && (!self.include_left || !self.include_right))) + { + self.close(); + return None; + } + + loop { + if !self.include_left { + let Some(current) = self.left.take() else { + self.close(); + return None; + }; + match current.next(false, &self.manager, self.hint) { + Ok(left) => { + self.left = left; + } + Err(err) => { + return Some(Err(err)); + } + } + } + // Return None if the next state is None + if self.left.is_none() { + self.close(); + return None; + } + + if let ( + Some(Leaf { + page: left_page, + entry: left_entry, + .. + }), + Some(Leaf { + page: right_page, + entry: right_entry, + .. + }), + ) = (&self.left, &self.right) + && left_page.get_page_number() == right_page.get_page_number() + && (left_entry > right_entry || (left_entry == right_entry && !self.include_right)) + { + self.close(); + return None; + } + + self.include_left = false; + if self.left.as_ref().unwrap().get_entry::().is_some() { + return self.left.as_ref().map(|s| s.get_entry().unwrap()).map(Ok); + } + } + } +} + +impl DoubleEndedIterator for BtreeRangeIter { + fn next_back(&mut self) -> Option { + // Lazily initialize the unbounded right boundary on first next_back() call. + if let Some(root) = self.uninit_right_root.take() { + let page = match self.manager.get_page(root, self.hint) { + Ok(p) => p, + Err(e) => return Some(Err(e)), + }; + match find_iter_unbounded::(page, None, true, &self.manager, self.hint) { + Ok(state) => self.right = state, + Err(e) => return Some(Err(e)), + } + } + if let ( + Some(Leaf { + page: left_page, + entry: left_entry, + .. + }), + Some(Leaf { + page: right_page, + entry: right_entry, + .. + }), + ) = (&self.left, &self.right) + && left_page.get_page_number() == right_page.get_page_number() + && (left_entry > right_entry + || (left_entry == right_entry && (!self.include_left || !self.include_right))) + { + self.close(); + return None; + } + + loop { + if !self.include_right { + let Some(current) = self.right.take() else { + self.close(); + return None; + }; + match current.next(true, &self.manager, self.hint) { + Ok(right) => { + self.right = right; + } + Err(err) => { + return Some(Err(err)); + } + } + } + // Return None if the next state is None + if self.right.is_none() { + self.close(); + return None; + } + + if let ( + Some(Leaf { + page: left_page, + entry: left_entry, + .. + }), + Some(Leaf { + page: right_page, + entry: right_entry, + .. + }), + ) = (&self.left, &self.right) + && left_page.get_page_number() == right_page.get_page_number() + && (left_entry > right_entry || (left_entry == right_entry && !self.include_left)) + { + self.close(); + return None; + } + + self.include_right = false; + if self.right.as_ref().unwrap().get_entry::().is_some() { + return self.right.as_ref().map(|s| s.get_entry().unwrap()).map(Ok); + } + } + } +} + +fn find_iter_unbounded( + page: PageImpl, + mut parent: Option>, + reverse: bool, + manager: &TransactionalMemory, + hint: PageHint, +) -> Result> { + let node_mem = page.memory(); + match node_mem[0] { + LEAF => { + let accessor = LeafAccessor::new(page.memory(), K::fixed_width(), V::fixed_width()); + let entry = if reverse { accessor.num_pairs() - 1 } else { 0 }; + Ok(Some(Leaf { + page, + fixed_key_size: K::fixed_width(), + fixed_value_size: V::fixed_width(), + entry, + parent, + })) + } + BRANCH => { + let accessor = BranchAccessor::new(&page, K::fixed_width()); + let child_index = if reverse { + accessor.count_children() - 1 + } else { + 0 + }; + let child_page_number = accessor.child_page(child_index).unwrap(); + let child_page = manager.get_page(child_page_number, hint)?; + let direction = if reverse { -1isize } else { 1 }; + parent = Some(Box::new(Internal { + page, + fixed_key_size: K::fixed_width(), + fixed_value_size: V::fixed_width(), + child: (isize::try_from(child_index).unwrap() + direction) + .try_into() + .unwrap(), + parent, + })); + find_iter_unbounded::(child_page, parent, reverse, manager, hint) + } + _ => unreachable!(), + } +} + +// Returns a bool indicating whether the first entry pointed to by the state is included in the +// queried range +fn find_iter_left( + page: PageImpl, + mut parent: Option>, + query: &[u8], + include_query: bool, + manager: &TransactionalMemory, + hint: PageHint, +) -> Result<(bool, Option)> { + let node_mem = page.memory(); + match node_mem[0] { + LEAF => { + let accessor = LeafAccessor::new(page.memory(), K::fixed_width(), V::fixed_width()); + let (mut position, found) = accessor.position::(query); + let include = if position < accessor.num_pairs() { + include_query || !found + } else { + // Back up to the last valid position + position -= 1; + // and exclude it + false + }; + let result = Leaf { + page, + fixed_key_size: K::fixed_width(), + fixed_value_size: V::fixed_width(), + entry: position, + parent, + }; + Ok((include, Some(result))) + } + BRANCH => { + let accessor = BranchAccessor::new(&page, K::fixed_width()); + let (child_index, child_page_number) = accessor.child_for_key::(query); + let child_page = manager.get_page(child_page_number, hint)?; + if child_index < accessor.count_children() - 1 { + parent = Some(Box::new(Internal { + page, + fixed_key_size: K::fixed_width(), + fixed_value_size: V::fixed_width(), + child: child_index + 1, + parent, + })); + } + find_iter_left::(child_page, parent, query, include_query, manager, hint) + } + _ => unreachable!(), + } +} + +fn find_iter_right( + page: PageImpl, + mut parent: Option>, + query: &[u8], + include_query: bool, + manager: &TransactionalMemory, + hint: PageHint, +) -> Result<(bool, Option)> { + let node_mem = page.memory(); + match node_mem[0] { + LEAF => { + let accessor = LeafAccessor::new(page.memory(), K::fixed_width(), V::fixed_width()); + let (mut position, found) = accessor.position::(query); + let include = if position < accessor.num_pairs() { + include_query && found + } else { + // Back up to the last valid position + position -= 1; + // and include it + true + }; + let result = Leaf { + page, + fixed_key_size: K::fixed_width(), + fixed_value_size: V::fixed_width(), + entry: position, + parent, + }; + Ok((include, Some(result))) + } + BRANCH => { + let accessor = BranchAccessor::new(&page, K::fixed_width()); + let (child_index, child_page_number) = accessor.child_for_key::(query); + let child_page = manager.get_page(child_page_number, hint)?; + if child_index > 0 && accessor.child_page(child_index - 1).is_some() { + parent = Some(Box::new(Internal { + page, + fixed_key_size: K::fixed_width(), + fixed_value_size: V::fixed_width(), + child: child_index - 1, + parent, + })); + } + find_iter_right::(child_page, parent, query, include_query, manager, hint) + } + _ => unreachable!(), + } +} diff --git a/v2/redb/src/tree_store/btree_mutator.rs b/v2/redb/src/tree_store/btree_mutator.rs new file mode 100644 index 000000000..d84f17068 --- /dev/null +++ b/v2/redb/src/tree_store/btree_mutator.rs @@ -0,0 +1,1076 @@ +use crate::tree_store::btree_base::{ + BRANCH, BranchAccessor, BranchBuilder, BranchMutator, Checksum, DEFERRED, LEAF, LeafAccessor, + LeafBuilder, LeafMutator, +}; +use crate::tree_store::btree_mutator::DeletionResult::{ + DeletedBranch, DeletedLeaf, PartialBranch, PartialLeaf, Subtree, +}; +use crate::tree_store::page_store::{Page, PageImpl, PageMut}; +use crate::tree_store::{ + AccessGuardMutInPlace, BtreeHeader, PageHint, PageNumber, PageTrackerPolicy, RawLeafBuilder, + TransactionalMemory, +}; +use crate::types::{Key, Value}; +use crate::{AccessGuard, Result}; +use std::cmp::{max, min}; +use std::marker::PhantomData; +use std::sync::{Arc, Mutex}; + +// TODO: it seems like Checksum can be removed from most/all of these, now that we're using deferred checksums +#[derive(Debug)] +enum DeletionResult { + // A proper subtree + Subtree(PageNumber, Checksum), + // A leaf with zero children + DeletedLeaf, + // A leaf with fewer entries than desired + PartialLeaf { + page: Arc<[u8]>, + deleted_pair: usize, + }, + // A branch page subtree with fewer children than desired. + // Held in unbuilt form: the caller will merge it with a sibling and build a new page, + // so allocating a page here just to free it again would be wasteful. + PartialBranch { + children: Vec<(PageNumber, Checksum)>, + keys: Vec>, + }, + // Indicates that the branch node was deleted, and includes the only remaining child + DeletedBranch(PageNumber, Checksum), +} + +struct InsertionResult<'a, V: Value + 'static> { + // the new root page + new_root: PageNumber, + // checksum of the root page + root_checksum: Checksum, + // Following sibling, if the root had to be split + additional_sibling: Option<(Vec, PageNumber, Checksum)>, + // The inserted value for .insert_reserve() to use + inserted_value: AccessGuardMutInPlace<'a, V>, + // The previous value, if any + old_value: Option>, +} + +pub(crate) struct MutateHelper<'a, 'b, K: Key, V: Value> { + root: &'b mut Option, + modify_uncommitted: bool, + mem: Arc, + freed: &'b mut Vec, + allocated: Arc>, + _key_type: PhantomData, + _value_type: PhantomData, + _lifetime: PhantomData<&'a ()>, +} + +impl<'a, 'b, K: Key, V: Value> MutateHelper<'a, 'b, K, V> { + pub(crate) fn new( + root: &'b mut Option, + mem: Arc, + freed: &'b mut Vec, + allocated: Arc>, + ) -> Self { + Self { + root, + modify_uncommitted: true, + mem, + freed, + allocated, + _key_type: Default::default(), + _value_type: Default::default(), + _lifetime: Default::default(), + } + } + + // Creates a new mutator which will not modify any existing uncommitted pages, or free any existing pages. + // It will still queue pages for future freeing in the freed vec + pub(crate) fn new_do_not_modify( + root: &'b mut Option, + mem: Arc, + freed: &'b mut Vec, + allocated: Arc>, + ) -> Self { + Self { + root, + modify_uncommitted: false, + mem, + freed, + allocated, + _key_type: Default::default(), + _value_type: Default::default(), + _lifetime: Default::default(), + } + } + + fn conditional_free(&mut self, page_number: PageNumber) { + if self.modify_uncommitted { + let mut allocated = self.allocated.lock().unwrap(); + if !self.mem.free_if_uncommitted(page_number, &mut allocated) { + self.freed.push(page_number); + } + } else { + self.freed.push(page_number); + } + } + + pub(crate) fn delete(&mut self, key: &K::SelfType<'_>) -> Result>> { + if let Some(BtreeHeader { + root: p, + checksum, + length, + }) = *self.root + { + let (deletion_result, found) = self.delete_helper( + self.mem.get_page(p, PageHint::None)?, + checksum, + K::as_bytes(key).as_ref(), + )?; + let new_length = if found.is_some() { length - 1 } else { length }; + let new_root = match deletion_result { + Subtree(page, checksum) => Some(BtreeHeader::new(page, checksum, new_length)), + DeletedLeaf => None, + PartialLeaf { page, deleted_pair } => { + let accessor = LeafAccessor::new(&page, K::fixed_width(), V::fixed_width()); + let mut builder = LeafBuilder::new( + &self.mem, + &self.allocated, + accessor.num_pairs() - 1, + K::fixed_width(), + V::fixed_width(), + ); + builder.push_all_except(&accessor, Some(deleted_pair)); + let page = builder.build()?; + assert_eq!(new_length, accessor.num_pairs() as u64 - 1); + Some(BtreeHeader::new( + page.get_page_number(), + DEFERRED, + new_length, + )) + } + PartialBranch { children, keys } => { + let mut builder = BranchBuilder::new( + &self.mem, + &self.allocated, + children.len(), + K::fixed_width(), + ); + for (child, child_checksum) in children { + builder.push_child(child, child_checksum); + } + for key in &keys { + builder.push_key(key); + } + let page = builder.build()?; + Some(BtreeHeader::new( + page.get_page_number(), + DEFERRED, + new_length, + )) + } + DeletedBranch(remaining_child, checksum) => { + Some(BtreeHeader::new(remaining_child, checksum, new_length)) + } + }; + *self.root = new_root; + Ok(found) + } else { + Ok(None) + } + } + + #[allow(clippy::type_complexity)] + pub(crate) fn insert( + &mut self, + key: &K::SelfType<'_>, + value: &V::SelfType<'_>, + ) -> Result<(Option>, AccessGuardMutInPlace<'a, V>)> { + let (new_root, old_value, guard) = if let Some(BtreeHeader { + root: p, + checksum, + length, + }) = *self.root + { + let result = self.insert_helper( + self.mem.get_page(p, PageHint::None)?, + checksum, + K::as_bytes(key).as_ref(), + V::as_bytes(value).as_ref(), + )?; + + let new_length = if result.old_value.is_some() { + length + } else { + length + 1 + }; + + let new_root = if let Some((key, page2, page2_checksum)) = result.additional_sibling { + let mut builder = + BranchBuilder::new(&self.mem, &self.allocated, 2, K::fixed_width()); + builder.push_child(result.new_root, result.root_checksum); + builder.push_key(&key); + builder.push_child(page2, page2_checksum); + let new_page = builder.build()?; + BtreeHeader::new(new_page.get_page_number(), DEFERRED, new_length) + } else { + BtreeHeader::new(result.new_root, result.root_checksum, new_length) + }; + (new_root, result.old_value, result.inserted_value) + } else { + let key_bytes = K::as_bytes(key); + let value_bytes = V::as_bytes(value); + let key_bytes = key_bytes.as_ref(); + let value_bytes = value_bytes.as_ref(); + let mut builder = LeafBuilder::new( + &self.mem, + &self.allocated, + 1, + K::fixed_width(), + V::fixed_width(), + ); + builder.push(key_bytes, value_bytes); + let page = builder.build()?; + + let accessor = LeafAccessor::new(page.memory(), K::fixed_width(), V::fixed_width()); + let offset = accessor.offset_of_first_value(); + let page_num = page.get_page_number(); + let guard = AccessGuardMutInPlace::new(page, offset, value_bytes.len()); + + (BtreeHeader::new(page_num, DEFERRED, 1), None, guard) + }; + *self.root = Some(new_root); + Ok((old_value, guard)) + } + + fn insert_helper( + &mut self, + page: PageImpl, + page_checksum: Checksum, + key: &[u8], + value: &[u8], + ) -> Result> { + let node_mem = page.memory(); + Ok(match node_mem[0] { + LEAF => { + let accessor = LeafAccessor::new(page.memory(), K::fixed_width(), V::fixed_width()); + let (position, found) = accessor.position::(key); + + // Fast-path to avoid re-building and splitting pages with a single large value + let single_large_value = accessor.num_pairs() == 1 + && accessor.total_length() >= self.mem.get_page_size(); + if !found && single_large_value { + let mut builder = LeafBuilder::new( + &self.mem, + &self.allocated, + 1, + K::fixed_width(), + V::fixed_width(), + ); + builder.push(key, value); + let new_page = builder.build()?; + let new_page_number = new_page.get_page_number(); + let new_page_accessor = + LeafAccessor::new(new_page.memory(), K::fixed_width(), V::fixed_width()); + let offset = new_page_accessor.offset_of_first_value(); + let guard = AccessGuardMutInPlace::new(new_page, offset, value.len()); + return if position == 0 { + Ok(InsertionResult { + new_root: new_page_number, + root_checksum: DEFERRED, + additional_sibling: Some(( + key.to_vec(), + page.get_page_number(), + page_checksum, + )), + inserted_value: guard, + old_value: None, + }) + } else { + let split_key = accessor.last_entry().key().to_vec(); + Ok(InsertionResult { + new_root: page.get_page_number(), + root_checksum: page_checksum, + additional_sibling: Some((split_key, new_page_number, DEFERRED)), + inserted_value: guard, + old_value: None, + }) + }; + } + + // Fast-path for uncommitted pages, that can be modified in-place + let has_inplace_space = || -> bool { + if found { + LeafMutator::sufficient_replace_inplace_space( + &page, + position, + K::fixed_width(), + V::fixed_width(), + value, + ) + } else { + LeafMutator::sufficient_insert_inplace_space( + &page, + position, + K::fixed_width(), + V::fixed_width(), + key, + value, + ) + } + }; + if self.mem.uncommitted(page.get_page_number()) + && self.modify_uncommitted + && has_inplace_space() + { + let page_number = page.get_page_number(); + let existing_value = if found { + let copied_value = accessor.entry(position).unwrap().value().to_vec(); + Some(AccessGuard::with_owned_value(copied_value)) + } else { + None + }; + drop(page); + let mut page_mut = self.mem.get_page_mut(page_number)?; + let mut mutator = + LeafMutator::new(page_mut.memory_mut(), K::fixed_width(), V::fixed_width()); + if found { + mutator.replace(position, value); + } else { + mutator.insert(position, key, value); + } + let new_page_accessor = + LeafAccessor::new(page_mut.memory(), K::fixed_width(), V::fixed_width()); + let offset = new_page_accessor.offset_of_value(position).unwrap(); + let guard = AccessGuardMutInPlace::new(page_mut, offset, value.len()); + return Ok(InsertionResult { + new_root: page_number, + root_checksum: DEFERRED, + additional_sibling: None, + inserted_value: guard, + old_value: existing_value, + }); + } + + let mut builder = LeafBuilder::new( + &self.mem, + &self.allocated, + accessor.num_pairs() + 1, + K::fixed_width(), + V::fixed_width(), + ); + for i in 0..accessor.num_pairs() { + if i == position { + builder.push(key, value); + } + if !found || i != position { + let entry = accessor.entry(i).unwrap(); + builder.push(entry.key(), entry.value()); + } + } + if accessor.num_pairs() == position { + builder.push(key, value); + } + if !builder.should_split() { + let new_page = builder.build()?; + + let page_number = page.get_page_number(); + let existing_value = if found { + let (start, end) = accessor.value_range(position).unwrap(); + if self.modify_uncommitted && self.mem.uncommitted(page_number) { + let arc = page.to_arc(); + drop(page); + let mut allocated = self.allocated.lock().unwrap(); + self.mem.free(page_number, &mut allocated); + Some(AccessGuard::with_arc_page(arc, start..end)) + } else { + self.freed.push(page_number); + Some(AccessGuard::with_page(page, start..end)) + } + } else { + drop(page); + self.conditional_free(page_number); + None + }; + + let new_page_number = new_page.get_page_number(); + let accessor = + LeafAccessor::new(new_page.memory(), K::fixed_width(), V::fixed_width()); + let offset = accessor.offset_of_value(position).unwrap(); + let guard = AccessGuardMutInPlace::new(new_page, offset, value.len()); + + InsertionResult { + new_root: new_page_number, + root_checksum: DEFERRED, + additional_sibling: None, + inserted_value: guard, + old_value: existing_value, + } + } else { + let (new_page1, split_key, new_page2) = builder.build_split()?; + let split_key = split_key.to_vec(); + let page_number = page.get_page_number(); + let existing_value = if found { + let (start, end) = accessor.value_range(position).unwrap(); + if self.modify_uncommitted && self.mem.uncommitted(page_number) { + let arc = page.to_arc(); + drop(page); + let mut allocated = self.allocated.lock().unwrap(); + self.mem.free(page_number, &mut allocated); + Some(AccessGuard::with_arc_page(arc, start..end)) + } else { + self.freed.push(page_number); + Some(AccessGuard::with_page(page, start..end)) + } + } else { + drop(page); + self.conditional_free(page_number); + None + }; + + let new_page_number = new_page1.get_page_number(); + let new_page_number2 = new_page2.get_page_number(); + let accessor = + LeafAccessor::new(new_page1.memory(), K::fixed_width(), V::fixed_width()); + let division = accessor.num_pairs(); + let guard = if position < division { + let accessor = LeafAccessor::new( + new_page1.memory(), + K::fixed_width(), + V::fixed_width(), + ); + let offset = accessor.offset_of_value(position).unwrap(); + AccessGuardMutInPlace::new(new_page1, offset, value.len()) + } else { + let accessor = LeafAccessor::new( + new_page2.memory(), + K::fixed_width(), + V::fixed_width(), + ); + let offset = accessor.offset_of_value(position - division).unwrap(); + AccessGuardMutInPlace::new(new_page2, offset, value.len()) + }; + + InsertionResult { + new_root: new_page_number, + root_checksum: DEFERRED, + additional_sibling: Some((split_key, new_page_number2, DEFERRED)), + inserted_value: guard, + old_value: existing_value, + } + } + } + BRANCH => { + let accessor = BranchAccessor::new(&page, K::fixed_width()); + let (child_index, child_page) = accessor.child_for_key::(key); + let child_checksum = accessor.child_checksum(child_index).unwrap(); + let sub_result = self.insert_helper( + self.mem.get_page(child_page, PageHint::None)?, + child_checksum, + key, + value, + )?; + + // Skip-path: if child page number and checksum haven't changed, + // no branch update is needed. This avoids redundant get_page_mut + + // write_child_page calls on repeat visits to the same subtree + // within a transaction. + if sub_result.additional_sibling.is_none() + && sub_result.new_root == child_page + && sub_result.root_checksum == child_checksum + { + return Ok(InsertionResult { + new_root: page.get_page_number(), + root_checksum: page_checksum, + additional_sibling: None, + inserted_value: sub_result.inserted_value, + old_value: sub_result.old_value, + }); + } + + if sub_result.additional_sibling.is_none() + && self.modify_uncommitted + && self.mem.uncommitted(page.get_page_number()) + { + let page_number = page.get_page_number(); + drop(page); + let mut mutpage = self.mem.get_page_mut(page_number)?; + let mut mutator = BranchMutator::new(mutpage.memory_mut()); + mutator.write_child_page( + child_index, + sub_result.new_root, + sub_result.root_checksum, + ); + return Ok(InsertionResult { + new_root: mutpage.get_page_number(), + root_checksum: DEFERRED, + additional_sibling: None, + inserted_value: sub_result.inserted_value, + old_value: sub_result.old_value, + }); + } + + // A child was added, or we couldn't use the fast-path above + let mut builder = BranchBuilder::new( + &self.mem, + &self.allocated, + accessor.count_children() + 1, + K::fixed_width(), + ); + if child_index == 0 { + builder.push_child(sub_result.new_root, sub_result.root_checksum); + if let Some((ref index_key2, page2, page2_checksum)) = + sub_result.additional_sibling + { + builder.push_key(index_key2); + builder.push_child(page2, page2_checksum); + } + } else { + builder.push_child( + accessor.child_page(0).unwrap(), + accessor.child_checksum(0).unwrap(), + ); + } + for i in 1..accessor.count_children() { + if let Some(key) = accessor.key(i - 1) { + builder.push_key(key); + if i == child_index { + builder.push_child(sub_result.new_root, sub_result.root_checksum); + if let Some((ref index_key2, page2, page2_checksum)) = + sub_result.additional_sibling + { + builder.push_key(index_key2); + builder.push_child(page2, page2_checksum); + } + } else { + builder.push_child( + accessor.child_page(i).unwrap(), + accessor.child_checksum(i).unwrap(), + ); + } + } else { + unreachable!(); + } + } + + let result = if builder.should_split() { + let (new_page1, split_key, new_page2) = builder.build_split()?; + InsertionResult { + new_root: new_page1.get_page_number(), + root_checksum: DEFERRED, + additional_sibling: Some(( + split_key.to_vec(), + new_page2.get_page_number(), + DEFERRED, + )), + inserted_value: sub_result.inserted_value, + old_value: sub_result.old_value, + } + } else { + let new_page = builder.build()?; + InsertionResult { + new_root: new_page.get_page_number(), + root_checksum: DEFERRED, + additional_sibling: None, + inserted_value: sub_result.inserted_value, + old_value: sub_result.old_value, + } + }; + // Free the original page, since we've replaced it + let page_number = page.get_page_number(); + drop(page); + self.conditional_free(page_number); + + result + } + _ => unreachable!(), + }) + } + + pub(crate) fn insert_inplace( + &mut self, + key: &K::SelfType<'_>, + value: &V::SelfType<'_>, + ) -> Result<()> { + assert!(self.modify_uncommitted); + let header = self.root.expect("Key not found (tree is empty)"); + self.insert_inplace_helper( + self.mem.get_page_mut(header.root)?, + K::as_bytes(key).as_ref(), + V::as_bytes(value).as_ref(), + )?; + *self.root = Some(BtreeHeader::new(header.root, DEFERRED, header.length)); + Ok(()) + } + + fn insert_inplace_helper(&mut self, mut page: PageMut, key: &[u8], value: &[u8]) -> Result<()> { + assert!(self.mem.uncommitted(page.get_page_number())); + + let node_mem = page.memory(); + match node_mem[0] { + LEAF => { + let accessor = LeafAccessor::new(page.memory(), K::fixed_width(), V::fixed_width()); + let (position, found) = accessor.position::(key); + assert!(found); + let old_len = accessor.entry(position).unwrap().value().len(); + assert!(value.len() <= old_len); + let mut mutator = + LeafMutator::new(page.memory_mut(), K::fixed_width(), V::fixed_width()); + mutator.replace(position, value); + } + BRANCH => { + let accessor = BranchAccessor::new(&page, K::fixed_width()); + let (child_index, child_page) = accessor.child_for_key::(key); + self.insert_inplace_helper(self.mem.get_page_mut(child_page)?, key, value)?; + let mut mutator = BranchMutator::new(page.memory_mut()); + mutator.write_child_page(child_index, child_page, DEFERRED); + } + _ => unreachable!(), + } + + Ok(()) + } + + fn delete_leaf_helper( + &mut self, + page: PageImpl, + checksum: Checksum, + key: &[u8], + ) -> Result<(DeletionResult, Option>)> { + let accessor = LeafAccessor::new(page.memory(), K::fixed_width(), V::fixed_width()); + let (position, found) = accessor.position::(key); + if !found { + return Ok((Subtree(page.get_page_number(), checksum), None)); + } + let new_kv_bytes = accessor.length_of_pairs(0, accessor.num_pairs()) + - accessor.length_of_pairs(position, position + 1); + let new_required_bytes = RawLeafBuilder::required_bytes( + accessor.num_pairs() - 1, + new_kv_bytes, + K::fixed_width(), + V::fixed_width(), + ); + let uncommitted = self.mem.uncommitted(page.get_page_number()); + + // Fast-path for dirty pages: perform in-place removal without allocating a new page. + // The threshold matches the merge threshold (page_size/3) so that we use in-place + // removal for all cases where the page won't need merging with a sibling. + if uncommitted + && self.modify_uncommitted + && new_required_bytes >= self.mem.get_page_size() / 3 + && accessor.num_pairs() > 1 + { + let (start, end) = accessor.value_range(position).unwrap(); + let page_number = page.get_page_number(); + drop(page); + let page_mut = self.mem.get_page_mut(page_number)?; + + let guard = AccessGuard::remove_on_drop( + page_mut, + start, + end - start, + position, + K::fixed_width(), + ); + return Ok((Subtree(page_number, DEFERRED), Some(guard))); + } + + let result = if accessor.num_pairs() == 1 { + DeletedLeaf + } else if new_required_bytes < self.mem.get_page_size() / 3 { + // Merge when less than 33% full. Splits occur when a page is full and produce two 50% + // full pages, so we use 33% instead of 50% to avoid oscillating + PartialLeaf { + page: page.to_arc(), + deleted_pair: position, + } + } else { + let mut builder = LeafBuilder::new( + &self.mem, + &self.allocated, + accessor.num_pairs() - 1, + K::fixed_width(), + V::fixed_width(), + ); + for i in 0..accessor.num_pairs() { + if i == position { + continue; + } + let entry = accessor.entry(i).unwrap(); + builder.push(entry.key(), entry.value()); + } + let new_page = builder.build()?; + Subtree(new_page.get_page_number(), DEFERRED) + }; + let (start, end) = accessor.value_range(position).unwrap(); + let guard = if uncommitted && self.modify_uncommitted { + let page_number = page.get_page_number(); + let arc = page.to_arc(); + drop(page); + let mut allocated = self.allocated.lock().unwrap(); + self.mem.free(page_number, &mut allocated); + Some(AccessGuard::with_arc_page(arc, start..end)) + } else { + // Won't be freed until the end of the transaction, so returning the page + // in the AccessGuard below is still safe + self.freed.push(page.get_page_number()); + Some(AccessGuard::with_page(page, start..end)) + }; + Ok((result, guard)) + } + + fn finalize_branch_builder( + builder: BranchBuilder<'_, '_>, + page_size: usize, + ) -> Result { + let result = if let Some((only_child, checksum)) = builder.to_single_child() { + DeletedBranch(only_child, checksum) + } else if builder.required_bytes() < page_size / 3 { + // Merge when less than 33% full. Splits occur when a page is full and produce two 50% + // full pages, so we use 33% instead of 50% to avoid oscillating. + // Skip the page allocation: the caller will immediately merge this with a sibling. + let (children, keys) = builder.into_parts(); + PartialBranch { children, keys } + } else { + let new_page = builder.build()?; + Subtree(new_page.get_page_number(), DEFERRED) + }; + Ok(result) + } + + fn delete_branch_helper( + &mut self, + page: PageImpl, + checksum: Checksum, + key: &[u8], + ) -> Result<(DeletionResult, Option>)> { + let accessor = BranchAccessor::new(&page, K::fixed_width()); + let original_page_number = page.get_page_number(); + let (child_index, child_page_number) = accessor.child_for_key::(key); + let child_checksum = accessor.child_checksum(child_index).unwrap(); + let (result, found) = self.delete_helper( + self.mem.get_page(child_page_number, PageHint::None)?, + child_checksum, + key, + )?; + if found.is_none() { + return Ok((Subtree(original_page_number, checksum), None)); + } + if let Subtree(new_child, new_child_checksum) = result { + // Skip-path: if child page number and checksum haven't changed, + // no branch update is needed. + if new_child == child_page_number && new_child_checksum == child_checksum { + return Ok((Subtree(original_page_number, checksum), found)); + } + + let result_page = + if self.mem.uncommitted(original_page_number) && self.modify_uncommitted { + drop(page); + let mut mutpage = self.mem.get_page_mut(original_page_number)?; + let mut mutator = BranchMutator::new(mutpage.memory_mut()); + mutator.write_child_page(child_index, new_child, new_child_checksum); + original_page_number + } else { + let mut builder = BranchBuilder::new( + &self.mem, + &self.allocated, + accessor.count_children(), + K::fixed_width(), + ); + builder.push_all(&accessor); + builder.replace_child(child_index, new_child, new_child_checksum); + let new_page = builder.build()?; + self.conditional_free(original_page_number); + new_page.get_page_number() + }; + return Ok((Subtree(result_page, DEFERRED), found)); + } + + // Child is requesting to be merged with a sibling + let mut builder = BranchBuilder::new( + &self.mem, + &self.allocated, + accessor.count_children(), + K::fixed_width(), + ); + + let final_result = match result { + Subtree(_, _) => { + // Handled in the if above + unreachable!(); + } + DeletedLeaf => { + for i in 0..accessor.count_children() { + if i == child_index { + continue; + } + builder.push_child( + accessor.child_page(i).unwrap(), + accessor.child_checksum(i).unwrap(), + ); + } + let end = if child_index == accessor.count_children() - 1 { + // Skip the last key, which precedes the child + accessor.count_children() - 2 + } else { + accessor.count_children() - 1 + }; + for i in 0..end { + if i == child_index { + continue; + } + builder.push_key(accessor.key(i).unwrap()); + } + Self::finalize_branch_builder(builder, self.mem.get_page_size())? + } + PartialLeaf { + page: partial_child_page, + deleted_pair, + } => { + let partial_child_accessor = + LeafAccessor::new(&partial_child_page, K::fixed_width(), V::fixed_width()); + debug_assert!(partial_child_accessor.num_pairs() > 1); + + let merge_with = if child_index == 0 { 1 } else { child_index - 1 }; + debug_assert!(merge_with < accessor.count_children()); + let merge_with_page = self + .mem + .get_page(accessor.child_page(merge_with).unwrap(), PageHint::None)?; + let merge_with_accessor = + LeafAccessor::new(merge_with_page.memory(), K::fixed_width(), V::fixed_width()); + + let single_large_value = merge_with_accessor.num_pairs() == 1 + && merge_with_accessor.total_length() >= self.mem.get_page_size(); + // Don't try to merge or rebalance, if the sibling contains a single large value + if single_large_value { + let mut child_builder = LeafBuilder::new( + &self.mem, + &self.allocated, + partial_child_accessor.num_pairs() - 1, + K::fixed_width(), + V::fixed_width(), + ); + child_builder.push_all_except(&partial_child_accessor, Some(deleted_pair)); + let new_page = child_builder.build()?; + builder.push_all(&accessor); + builder.replace_child(child_index, new_page.get_page_number(), DEFERRED); + + let result = Self::finalize_branch_builder(builder, self.mem.get_page_size())?; + + drop(page); + self.conditional_free(original_page_number); + // child_page_number does not need to be freed, because it's a leaf and the + // MutAccessGuard will free it + + return Ok((result, found)); + } + + for i in 0..accessor.count_children() { + if i == child_index { + continue; + } + let page_number = accessor.child_page(i).unwrap(); + let page_checksum = accessor.child_checksum(i).unwrap(); + if i == merge_with { + let mut child_builder = LeafBuilder::new( + &self.mem, + &self.allocated, + partial_child_accessor.num_pairs() - 1 + + merge_with_accessor.num_pairs(), + K::fixed_width(), + V::fixed_width(), + ); + if child_index < merge_with { + child_builder + .push_all_except(&partial_child_accessor, Some(deleted_pair)); + } + child_builder.push_all_except(&merge_with_accessor, None); + if child_index > merge_with { + child_builder + .push_all_except(&partial_child_accessor, Some(deleted_pair)); + } + if child_builder.should_split() { + let (new_page1, split_key, new_page2) = child_builder.build_split()?; + builder.push_key(split_key); + builder.push_child(new_page1.get_page_number(), DEFERRED); + builder.push_child(new_page2.get_page_number(), DEFERRED); + } else { + let new_page = child_builder.build()?; + builder.push_child(new_page.get_page_number(), DEFERRED); + } + + let merged_key_index = max(child_index, merge_with); + if merged_key_index < accessor.count_children() - 1 { + builder.push_key(accessor.key(merged_key_index).unwrap()); + } + } else { + builder.push_child(page_number, page_checksum); + if i < accessor.count_children() - 1 { + builder.push_key(accessor.key(i).unwrap()); + } + } + } + + let result = Self::finalize_branch_builder(builder, self.mem.get_page_size())?; + + let page_number = merge_with_page.get_page_number(); + drop(merge_with_page); + self.conditional_free(page_number); + // child_page_number does not need to be freed, because it's a leaf and the + // MutAccessGuard will free it + + result + } + DeletedBranch(only_grandchild, grandchild_checksum) => { + let merge_with = if child_index == 0 { 1 } else { child_index - 1 }; + let merge_with_page = self + .mem + .get_page(accessor.child_page(merge_with).unwrap(), PageHint::None)?; + let merge_with_accessor = BranchAccessor::new(&merge_with_page, K::fixed_width()); + debug_assert!(merge_with < accessor.count_children()); + for i in 0..accessor.count_children() { + if i == child_index { + continue; + } + let page_number = accessor.child_page(i).unwrap(); + let page_checksum = accessor.child_checksum(i).unwrap(); + if i == merge_with { + let mut child_builder = BranchBuilder::new( + &self.mem, + &self.allocated, + merge_with_accessor.count_children() + 1, + K::fixed_width(), + ); + let separator_key = accessor.key(min(child_index, merge_with)).unwrap(); + if child_index < merge_with { + child_builder.push_child(only_grandchild, grandchild_checksum); + child_builder.push_key(separator_key); + } + child_builder.push_all(&merge_with_accessor); + if child_index > merge_with { + child_builder.push_key(separator_key); + child_builder.push_child(only_grandchild, grandchild_checksum); + } + if child_builder.should_split() { + let (new_page1, separator, new_page2) = child_builder.build_split()?; + builder.push_child(new_page1.get_page_number(), DEFERRED); + builder.push_key(separator); + builder.push_child(new_page2.get_page_number(), DEFERRED); + } else { + let new_page = child_builder.build()?; + builder.push_child(new_page.get_page_number(), DEFERRED); + } + + let merged_key_index = max(child_index, merge_with); + if merged_key_index < accessor.count_children() - 1 { + builder.push_key(accessor.key(merged_key_index).unwrap()); + } + } else { + builder.push_child(page_number, page_checksum); + if i < accessor.count_children() - 1 { + builder.push_key(accessor.key(i).unwrap()); + } + } + } + let result = Self::finalize_branch_builder(builder, self.mem.get_page_size())?; + + let page_number = merge_with_page.get_page_number(); + drop(merge_with_page); + self.conditional_free(page_number); + + result + } + PartialBranch { + children: partial_children, + keys: partial_keys, + } => { + let merge_with = if child_index == 0 { 1 } else { child_index - 1 }; + let merge_with_page = self + .mem + .get_page(accessor.child_page(merge_with).unwrap(), PageHint::None)?; + let merge_with_accessor = BranchAccessor::new(&merge_with_page, K::fixed_width()); + debug_assert!(merge_with < accessor.count_children()); + for i in 0..accessor.count_children() { + if i == child_index { + continue; + } + let page_number = accessor.child_page(i).unwrap(); + let page_checksum = accessor.child_checksum(i).unwrap(); + if i == merge_with { + let mut child_builder = BranchBuilder::new( + &self.mem, + &self.allocated, + merge_with_accessor.count_children() + partial_children.len(), + K::fixed_width(), + ); + let separator_key = accessor.key(min(child_index, merge_with)).unwrap(); + if child_index < merge_with { + for &(child, child_checksum) in &partial_children { + child_builder.push_child(child, child_checksum); + } + for key in &partial_keys { + child_builder.push_key(key); + } + child_builder.push_key(separator_key); + } + child_builder.push_all(&merge_with_accessor); + if child_index > merge_with { + child_builder.push_key(separator_key); + for &(child, child_checksum) in &partial_children { + child_builder.push_child(child, child_checksum); + } + for key in &partial_keys { + child_builder.push_key(key); + } + } + if child_builder.should_split() { + let (new_page1, separator, new_page2) = child_builder.build_split()?; + builder.push_child(new_page1.get_page_number(), DEFERRED); + builder.push_key(separator); + builder.push_child(new_page2.get_page_number(), DEFERRED); + } else { + let new_page = child_builder.build()?; + builder.push_child(new_page.get_page_number(), DEFERRED); + } + + let merged_key_index = max(child_index, merge_with); + if merged_key_index < accessor.count_children() - 1 { + builder.push_key(accessor.key(merged_key_index).unwrap()); + } + } else { + builder.push_child(page_number, page_checksum); + if i < accessor.count_children() - 1 { + builder.push_key(accessor.key(i).unwrap()); + } + } + } + let result = Self::finalize_branch_builder(builder, self.mem.get_page_size())?; + + let page_number = merge_with_page.get_page_number(); + drop(merge_with_page); + self.conditional_free(page_number); + + result + } + }; + + drop(page); + self.conditional_free(original_page_number); + + Ok((final_result, found)) + } + + // Returns the page number of the sub-tree with this key deleted, or None if the sub-tree is empty. + // If key is not found, guaranteed not to modify the tree + fn delete_helper( + &mut self, + page: PageImpl, + checksum: Checksum, + key: &[u8], + ) -> Result<(DeletionResult, Option>)> { + let node_mem = page.memory(); + match node_mem[0] { + LEAF => self.delete_leaf_helper(page, checksum, key), + BRANCH => self.delete_branch_helper(page, checksum, key), + _ => unreachable!(), + } + } +} diff --git a/v2/redb/src/tree_store/mod.rs b/v2/redb/src/tree_store/mod.rs new file mode 100644 index 000000000..355c5b0c3 --- /dev/null +++ b/v2/redb/src/tree_store/mod.rs @@ -0,0 +1,25 @@ +mod btree; +mod btree_base; +mod btree_iters; +mod btree_mutator; +mod page_store; +mod table_tree; +mod table_tree_base; + +pub(crate) use btree::{ + Btree, BtreeMut, BtreeStats, PagePath, RawBtree, UntypedBtree, UntypedBtreeMut, btree_stats, +}; +pub use btree_base::{AccessGuard, AccessGuardMut, AccessGuardMutInPlace}; +pub(crate) use btree_base::{ + BRANCH, BranchAccessor, BranchMutator, BtreeHeader, Checksum, DEFERRED, LEAF, LeafAccessor, + LeafMutator, RawLeafBuilder, +}; +pub(crate) use btree_iters::{AllPageNumbersBtreeIter, BtreeExtractIf, BtreeRangeIter}; +pub(crate) use page_store::ReadOnlyBackend; +pub(crate) use page_store::{ + FILE_FORMAT_VERSION3, MAX_PAIR_LENGTH, MAX_VALUE_LENGTH, PAGE_SIZE, Page, PageHint, PageNumber, + PageTrackerPolicy, SerializedSavepoint, ShrinkPolicy, TransactionalMemory, +}; +pub use page_store::{InMemoryBackend, Savepoint, file_backend}; +pub(crate) use table_tree::{PageListMut, TableTree, TableTreeMut}; +pub(crate) use table_tree_base::{InternalTableDefinition, TableType}; diff --git a/v2/redb/src/tree_store/page_store/backends.rs b/v2/redb/src/tree_store/page_store/backends.rs new file mode 100644 index 000000000..196bc5d3e --- /dev/null +++ b/v2/redb/src/tree_store/page_store/backends.rs @@ -0,0 +1,107 @@ +use crate::StorageBackend; +use std::io; +use std::io::Error; +use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; + +#[derive(Debug)] +pub(crate) struct ReadOnlyBackend { + inner: Box, +} + +impl ReadOnlyBackend { + pub fn new(inner: Box) -> Self { + Self { inner } + } +} + +impl StorageBackend for ReadOnlyBackend { + fn len(&self) -> Result { + self.inner.len() + } + + fn read(&self, offset: u64, out: &mut [u8]) -> Result<(), Error> { + self.inner.read(offset, out) + } + + fn set_len(&self, _len: u64) -> Result<(), Error> { + unreachable!() + } + + fn sync_data(&self) -> Result<(), Error> { + unreachable!() + } + + fn write(&self, _offset: u64, _data: &[u8]) -> Result<(), Error> { + unreachable!() + } + + fn close(&self) -> Result<(), Error> { + self.inner.close() + } +} + +/// Acts as temporal in-memory database storage. +#[derive(Debug, Default)] +pub struct InMemoryBackend(RwLock>); + +impl InMemoryBackend { + fn out_of_range() -> io::Error { + io::Error::new(io::ErrorKind::InvalidInput, "Index out-of-range.") + } +} + +impl InMemoryBackend { + /// Creates a new, empty memory backend. + pub fn new() -> Self { + Self::default() + } + + /// Gets a read guard for this backend. + fn read(&self) -> RwLockReadGuard<'_, Vec> { + self.0.read().expect("Could not acquire read lock.") + } + + /// Gets a write guard for this backend. + fn write(&self) -> RwLockWriteGuard<'_, Vec> { + self.0.write().expect("Could not acquire write lock.") + } +} + +impl StorageBackend for InMemoryBackend { + fn len(&self) -> Result { + Ok(self.read().len() as u64) + } + + fn read(&self, offset: u64, out: &mut [u8]) -> Result<(), io::Error> { + let guard = self.read(); + let offset = usize::try_from(offset).map_err(|_| Self::out_of_range())?; + if offset + out.len() <= guard.len() { + out.copy_from_slice(&guard[offset..offset + out.len()]); + Ok(()) + } else { + Err(Self::out_of_range()) + } + } + + fn set_len(&self, len: u64) -> Result<(), io::Error> { + let mut guard = self.write(); + let len = usize::try_from(len).map_err(|_| Self::out_of_range())?; + guard.resize(len, 0); + Ok(()) + } + + fn sync_data(&self) -> Result<(), io::Error> { + Ok(()) + } + + fn write(&self, offset: u64, data: &[u8]) -> Result<(), io::Error> { + let mut guard = self.write(); + let offset = usize::try_from(offset).map_err(|_| Self::out_of_range())?; + if offset + data.len() <= guard.len() { + guard[offset..offset + data.len()].copy_from_slice(data); + Ok(()) + } else { + Err(Self::out_of_range()) + } + } +} diff --git a/v2/redb/src/tree_store/page_store/base.rs b/v2/redb/src/tree_store/page_store/base.rs new file mode 100644 index 000000000..07e66f105 --- /dev/null +++ b/v2/redb/src/tree_store/page_store/base.rs @@ -0,0 +1,376 @@ +use crate::tree_store::page_store::cached_file::WritablePage; +use crate::tree_store::page_store::page_manager::MAX_MAX_PAGE_ORDER; +use std::cmp::Ordering; +#[cfg(debug_assertions)] +use std::collections::HashMap; +use std::collections::HashSet; +use std::fmt::{Debug, Formatter}; +use std::hash::{Hash, Hasher}; +use std::marker::PhantomData; +use std::mem; +use std::ops::Range; +use std::sync::Arc; +#[cfg(debug_assertions)] +use std::sync::Mutex; + +pub(crate) const MAX_VALUE_LENGTH: usize = 3 * 1024 * 1024 * 1024; +pub(crate) const MAX_PAIR_LENGTH: usize = 3 * 1024 * 1024 * 1024 + 768 * 1024 * 1024; +pub(crate) const MAX_PAGE_INDEX: u32 = 0x000F_FFFF; +pub(crate) const MAX_REGIONS: u32 = 0x0010_0000; + +// On-disk format is: +// TODO: consider implementing an optimization in which we store the number of order-0 pages that +// are actually used, in these reserved bits, so that the reads to the PagedCachedFile layer can avoid +// reading all the zeros at the end of the page. +// lowest 20bits: page index within the region. Only the lowest `20 - order_exponent` bits may be read. +// The remaining bits are reserved for future use and must be ignored +// second 20bits: region number +// 19bits: reserved +// highest 5bits: page order exponent +// +// Assuming a reasonable page size, like 4kiB, this allows for 4kiB * 2^20 * 2^20 = 4PiB of usable space +#[derive(Copy, Clone, Eq, PartialEq)] +pub(crate) struct PageNumber { + pub(crate) region: u32, + pub(crate) page_index: u32, + pub(crate) page_order: u8, +} + +impl Hash for PageNumber { + fn hash(&self, state: &mut H) { + let mut temp = 0x000F_FFFF & u64::from(self.page_index); + temp |= (0x000F_FFFF & u64::from(self.region)) << 20; + temp |= (0b0001_1111 & u64::from(self.page_order)) << 59; + state.write_u64(temp); + } +} + +// PageNumbers are ordered as determined by their starting address in the database file +impl Ord for PageNumber { + fn cmp(&self, other: &Self) -> Ordering { + match self.region.cmp(&other.region) { + Ordering::Less => Ordering::Less, + Ordering::Equal => { + let self_order0 = self.page_index * 2u32.pow(self.page_order.into()); + let other_order0 = other.page_index * 2u32.pow(other.page_order.into()); + assert!( + self_order0 != other_order0 || self.page_order == other.page_order, + "{self:?} overlaps {other:?}, but is not equal" + ); + self_order0.cmp(&other_order0) + } + Ordering::Greater => Ordering::Greater, + } + } +} + +impl PartialOrd for PageNumber { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PageNumber { + pub(crate) const fn serialized_size() -> usize { + 8 + } + + pub(crate) fn new(region: u32, page_index: u32, page_order: u8) -> Self { + debug_assert!(region <= 0x000F_FFFF); + debug_assert!(page_index <= MAX_PAGE_INDEX); + debug_assert!(page_order <= MAX_MAX_PAGE_ORDER); + Self { + region, + page_index, + page_order, + } + } + + pub(crate) fn to_le_bytes(self) -> [u8; 8] { + let mut temp = 0x000F_FFFF & u64::from(self.page_index); + temp |= (0x000F_FFFF & u64::from(self.region)) << 20; + temp |= (0b0001_1111 & u64::from(self.page_order)) << 59; + temp.to_le_bytes() + } + + pub(crate) fn from_le_bytes(bytes: [u8; 8]) -> Self { + let temp = u64::from_le_bytes(bytes); + let order = (temp >> 59) as u8; + let index = u32::try_from(temp & (0x000F_FFFF >> order)).unwrap(); + let region = ((temp >> 20) & 0x000F_FFFF) as u32; + + Self { + region, + page_index: index, + page_order: order, + } + } + + #[cfg(test)] + pub(crate) fn to_order0(self) -> Vec { + let mut pages = vec![self]; + loop { + let mut progress = false; + let mut new_pages = vec![]; + for page in pages { + if page.page_order == 0 { + new_pages.push(page); + } else { + progress = true; + new_pages.push(PageNumber::new( + page.region, + page.page_index * 2, + page.page_order - 1, + )); + new_pages.push(PageNumber::new( + page.region, + page.page_index * 2 + 1, + page.page_order - 1, + )); + } + } + pages = new_pages; + if !progress { + break; + } + } + + pages + } + + pub(crate) fn address_range( + &self, + data_section_offset: u64, + region_size: u64, + region_pages_start: u64, + page_size: u32, + ) -> Range { + let regional_start = + region_pages_start + u64::from(self.page_index) * self.page_size_bytes(page_size); + debug_assert!(regional_start < region_size); + let region_base = u64::from(self.region) * region_size; + let start = data_section_offset + region_base + regional_start; + let end = start + self.page_size_bytes(page_size); + start..end + } + + pub(crate) fn page_size_bytes(&self, page_size: u32) -> u64 { + let pages = 1u64 << self.page_order; + pages * u64::from(page_size) + } +} + +impl Debug for PageNumber { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "r{}.{}/{}", + self.region, self.page_index, self.page_order + ) + } +} + +pub(crate) trait Page { + fn memory(&self) -> &[u8]; + + fn get_page_number(&self) -> PageNumber; +} + +pub struct PageImpl { + pub(super) mem: Arc<[u8]>, + pub(super) page_number: PageNumber, + #[cfg(debug_assertions)] + pub(super) open_pages: Arc>>, +} + +impl PageImpl { + pub(crate) fn to_arc(&self) -> Arc<[u8]> { + self.mem.clone() + } +} + +impl Debug for PageImpl { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("PageImpl: page_number={:?}", self.page_number)) + } +} + +#[cfg(debug_assertions)] +impl Drop for PageImpl { + fn drop(&mut self) { + let mut open_pages = self.open_pages.lock().unwrap(); + let value = open_pages.get_mut(&self.page_number).unwrap(); + assert!(*value > 0); + *value -= 1; + if *value == 0 { + open_pages.remove(&self.page_number); + } + } +} + +impl Page for PageImpl { + fn memory(&self) -> &[u8] { + self.mem.as_ref() + } + + fn get_page_number(&self) -> PageNumber { + self.page_number + } +} + +impl Clone for PageImpl { + fn clone(&self) -> Self { + #[cfg(debug_assertions)] + { + *self + .open_pages + .lock() + .unwrap() + .get_mut(&self.page_number) + .unwrap() += 1; + } + Self { + mem: self.mem.clone(), + page_number: self.page_number, + #[cfg(debug_assertions)] + open_pages: self.open_pages.clone(), + } + } +} + +// The lifetime should be bound to the lifetime of the transaction in which this page was opened. +// It is used in the Drop impl to ensure that the page is dropped before the transaction is committed. +pub(crate) struct PageMut<'txn> { + pub(super) mem: WritablePage, + pub(super) page_number: PageNumber, + pub(super) _lifetime: PhantomData<&'txn ()>, + #[cfg(debug_assertions)] + pub(super) open_pages: Arc>>, +} + +impl PageMut<'_> { + pub(crate) fn memory_mut(&mut self) -> &mut [u8] { + self.mem.mem_mut() + } +} + +impl Page for PageMut<'_> { + fn memory(&self) -> &[u8] { + self.mem.mem() + } + + fn get_page_number(&self) -> PageNumber { + self.page_number + } +} + +impl Drop for PageMut<'_> { + fn drop(&mut self) { + #[cfg(debug_assertions)] + assert!(self.open_pages.lock().unwrap().remove(&self.page_number)); + } +} + +#[derive(Copy, Clone)] +pub(crate) enum PageHint { + None, + Clean, +} + +pub(crate) enum PageTrackerPolicy { + Ignore, + Track(HashSet), + Closed, +} + +impl PageTrackerPolicy { + pub(crate) fn new_tracking() -> Self { + PageTrackerPolicy::Track(HashSet::new()) + } + + pub(crate) fn is_empty(&self) -> bool { + match self { + PageTrackerPolicy::Ignore | PageTrackerPolicy::Closed => true, + PageTrackerPolicy::Track(x) => x.is_empty(), + } + } + + pub(super) fn remove(&mut self, page: PageNumber) { + match self { + PageTrackerPolicy::Ignore => {} + PageTrackerPolicy::Track(x) => { + assert!(x.remove(&page)); + } + PageTrackerPolicy::Closed => { + panic!("Page tracker is closed"); + } + } + } + + pub(super) fn insert(&mut self, page: PageNumber) { + match self { + PageTrackerPolicy::Ignore => {} + PageTrackerPolicy::Track(x) => { + assert!(x.insert(page)); + } + PageTrackerPolicy::Closed => { + panic!("Page tracker is closed"); + } + } + } + + pub(crate) fn close(&mut self) -> HashSet { + let old = mem::replace(self, PageTrackerPolicy::Closed); + match old { + PageTrackerPolicy::Ignore => HashSet::new(), + PageTrackerPolicy::Track(x) => x, + PageTrackerPolicy::Closed => { + panic!("Page tracker is closed"); + } + } + } + + pub(crate) fn reset(&mut self) -> HashSet { + if matches!(self, PageTrackerPolicy::Ignore) { + return HashSet::new(); + } + let old = mem::replace(self, PageTrackerPolicy::Track(HashSet::new())); + match old { + PageTrackerPolicy::Ignore => HashSet::new(), + PageTrackerPolicy::Track(x) => x, + PageTrackerPolicy::Closed => { + panic!("Page tracker is closed"); + } + } + } +} + +#[cfg(test)] +mod test { + use crate::tree_store::PageNumber; + + #[test] + fn last_page() { + let region_data_size = 2u64.pow(32); + let page_size = 4096; + let pages_per_region = region_data_size / page_size; + let region_header_size = 2u64.pow(16); + let last_page_index = pages_per_region - 1; + let page_number = PageNumber::new(1, last_page_index.try_into().unwrap(), 0); + page_number.address_range( + 4096, + region_data_size + region_header_size, + region_header_size, + page_size.try_into().unwrap(), + ); + } + + #[test] + fn reserved_bits() { + let page_number = PageNumber::new(0, 0, 12); + let mut bytes = page_number.to_le_bytes(); + bytes[1] = 0xFF; + let page_number2 = PageNumber::from_le_bytes(bytes); + assert_eq!(page_number, page_number2); + } +} diff --git a/v2/redb/src/tree_store/page_store/bitmap.rs b/v2/redb/src/tree_store/page_store/bitmap.rs new file mode 100644 index 000000000..ce49187d3 --- /dev/null +++ b/v2/redb/src/tree_store/page_store/bitmap.rs @@ -0,0 +1,534 @@ +use crate::tree_store::page_store::xxh3_checksum; +use std::mem::size_of; + +const HEIGHT_OFFSET: usize = 0; +const END_OFFSETS: usize = HEIGHT_OFFSET + size_of::(); + +pub(crate) struct BtreeBitmap { + heights: Vec, +} + +// Stores a 64-way bit-tree of allocated ids. +// +// Data structure format: +// height: u32 +// layer_ends: array of u32, ending offset in bytes of layers. +// layer data: u64s +// ...consecutive layers. Except for the last level, all sub-trees of the root must be complete +impl BtreeBitmap { + pub(crate) fn count_unset(&self) -> u32 { + self.get_level(self.get_height() - 1).count_unset() + } + + pub(crate) fn has_unset(&self) -> bool { + self.get_level(self.get_height() - 1).any_unset() + } + + pub(crate) fn get(&self, i: u32) -> bool { + self.get_level(self.get_height() - 1).get(i) + } + + pub(crate) fn len(&self) -> u32 { + self.get_level(self.get_height() - 1).len() + } + + pub(crate) fn find_first_unset(&self) -> Option { + if let Some(mut entry) = self.get_level(0).first_unset(0, 64) { + let mut height = 0; + + while height < self.get_height() - 1 { + height += 1; + entry *= 64; + entry = self + .get_level(height) + .first_unset(entry, entry + 64) + .unwrap(); + } + + Some(entry) + } else { + None + } + } + + fn get_level(&self, i: u32) -> &U64GroupedBitmap { + assert!(i < self.get_height()); + &self.heights[i as usize] + } + + fn get_height(&self) -> u32 { + self.heights.len().try_into().unwrap() + } + + pub(crate) fn xxh3_hash(&self) -> u128 { + let mut result = 0; + for height in &self.heights { + result ^= height.xxh3_hash(); + } + result + } + + pub(crate) fn to_vec(&self) -> Vec { + let mut result = vec![]; + let height: u32 = self.heights.len().try_into().unwrap(); + result.extend(height.to_le_bytes()); + + let vecs: Vec> = self.heights.iter().map(|x| x.to_vec()).collect(); + let mut data_offset = END_OFFSETS + self.heights.len() * size_of::(); + let end_metadata = data_offset; + for serialized in &vecs { + data_offset += serialized.len(); + let offset_u32: u32 = data_offset.try_into().unwrap(); + result.extend(offset_u32.to_le_bytes()); + } + + assert_eq!(end_metadata, result.len()); + for serialized in &vecs { + result.extend(serialized); + } + + result + } + + pub(crate) fn from_bytes(data: &[u8]) -> Self { + let height = u32::from_le_bytes( + data[HEIGHT_OFFSET..(HEIGHT_OFFSET + size_of::())] + .try_into() + .unwrap(), + ); + + let mut metadata = END_OFFSETS; + let mut data_start = END_OFFSETS + (height as usize) * size_of::(); + + let mut heights = vec![]; + for _ in 0..height { + let data_end = u32::from_le_bytes( + data[metadata..(metadata + size_of::())] + .try_into() + .unwrap(), + ) as usize; + heights.push(U64GroupedBitmap::from_bytes(&data[data_start..data_end])); + data_start = data_end; + metadata += size_of::(); + } + + Self { heights } + } + + // Initializes a new allocator, with no ids free + pub(crate) fn new(mut num_pages: u32, mut capacity: u32) -> Self { + let mut heights = vec![]; + + // Build from the leaf to root + loop { + heights.push(U64GroupedBitmap::new_full(num_pages, capacity)); + if capacity <= 64 { + break; + } + capacity = capacity.div_ceil(64); + num_pages = num_pages.div_ceil(64); + } + + // Reverse so that the root is at index 0 + heights.reverse(); + + Self { heights } + } + + // Like new(), but pads the tree height for max_capacity so resize() + // never needs to insert new levels. + pub(crate) fn new_padded(num_pages: u32, capacity: u32, max_capacity: u32) -> Self { + let mut result = Self::new(num_pages, capacity); + + let max_height = Self::height_for_capacity(max_capacity); + while result.heights.len() < max_height { + let root_len = result.heights[0].len(); + let parent_len = root_len.div_ceil(64); + result + .heights + .insert(0, U64GroupedBitmap::new_full(parent_len, parent_len)); + } + + result + } + + fn height_for_capacity(mut capacity: u32) -> usize { + let mut height = 1; + while capacity > 64 { + capacity = capacity.div_ceil(64); + height += 1; + } + height + } + + pub(crate) fn resize(&mut self, mut new_len: u32, full: bool) { + for height in self.heights.iter_mut().rev() { + height.resize(new_len, full); + new_len = new_len.div_ceil(64); + } + assert!(self.get_level(0).len() <= 64); + } + + // Returns the first unset id, and sets it + pub(crate) fn alloc(&mut self) -> Option { + let entry = self.find_first_unset()?; + self.set(entry); + Some(entry) + } + + pub(crate) fn set(&mut self, i: u32) { + let full = self.get_level_mut(self.get_height() - 1).set(i); + self.update_to_root(i, full); + } + + pub(crate) fn clear(&mut self, i: u32) { + self.get_level_mut(self.get_height() - 1).clear(i); + self.update_to_root(i, false); + } + + fn get_level_mut(&mut self, i: u32) -> &mut U64GroupedBitmap { + assert!(i < self.get_height()); + &mut self.heights[i as usize] + } + + // Recursively update to the root, starting at the given entry in the given height + // full parameter must be set if all bits in the entry's group of u64 are full + fn update_to_root(&mut self, i: u32, mut full: bool) { + if self.get_height() == 1 { + return; + } + + let mut parent_height = self.get_height() - 2; + let mut parent_entry = i / 64; + loop { + full = if full { + self.get_level_mut(parent_height).set(parent_entry) + } else { + self.get_level_mut(parent_height).clear(parent_entry); + false + }; + + if parent_height == 0 { + break; + } + parent_height -= 1; + parent_entry /= 64; + } + } +} + +// A bitmap which groups consecutive groups of 64bits together +pub(crate) struct U64GroupedBitmap { + len: u32, + data: Vec, +} + +impl U64GroupedBitmap { + fn required_words(elements: u32) -> usize { + let words = elements.div_ceil(64); + words as usize + } + + pub fn new_full(len: u32, capacity: u32) -> Self { + let data = vec![u64::MAX; Self::required_words(capacity)]; + Self { len, data } + } + + pub fn xxh3_hash(&self) -> u128 { + if self.len == 0 { + return 0; + } + let mut bytes = vec![]; + bytes.extend(self.len.to_le_bytes()); + // Hash all the whole words + for x in &self.data[0..Self::required_words(self.len) - 1] { + bytes.extend(x.to_le_bytes()); + } + let (index, bit) = Self::data_index_of(self.len - 1); + // Select the bit and all lower ones + let mask = ((1 << bit) - 1) | (1 << bit); + let group = self.data[index]; + let group = group & mask; + bytes.extend(group.to_le_bytes()); + + xxh3_checksum(&bytes) + } + + // Format: + // 4 bytes: number of elements + // n bytes: serialized groups + pub fn to_vec(&self) -> Vec { + let words = Self::required_words(self.len); + let mut result = Vec::with_capacity(size_of::() + words * size_of::()); + result.extend_from_slice(&self.len.to_le_bytes()); + for x in &self.data[..words] { + result.extend_from_slice(&x.to_le_bytes()); + } + result + } + + pub fn from_bytes(serialized: &[u8]) -> Self { + assert_eq!(0, (serialized.len() - size_of::()) % size_of::()); + let len = u32::from_le_bytes(serialized[..size_of::()].try_into().unwrap()); + let words = (serialized.len() - size_of::()) / size_of::(); + let mut data = Vec::with_capacity(words); + for i in 0..words { + let start = size_of::() + i * size_of::(); + let value = u64::from_le_bytes( + serialized[start..(start + size_of::())] + .try_into() + .unwrap(), + ); + data.push(value); + } + + Self { len, data } + } + + fn data_index_of(bit: u32) -> (usize, usize) { + ((bit as usize) / 64, (bit as usize) % 64) + } + + fn select_mask(bit: usize) -> u64 { + 1u64 << (bit as u64) + } + + fn count_unset(&self) -> u32 { + self.data.iter().map(|x| x.count_zeros()).sum() + } + + fn any_unset(&self) -> bool { + self.data.iter().any(|x| x.count_zeros() > 0) + } + + fn first_unset(&self, start_bit: u32, end_bit: u32) -> Option { + assert_eq!(end_bit, (start_bit - start_bit % 64) + 64); + if self.len == 0 { + return None; + } + + let (index, bit) = Self::data_index_of(start_bit); + let mask = (1 << bit) - 1; + let group = self.data[index]; + let group = group | mask; + match group.trailing_ones() { + 64 => None, + x => Some(start_bit + x - u32::try_from(bit).unwrap()), + } + } + + pub fn len(&self) -> u32 { + self.len + } + + pub fn resize(&mut self, new_len: u32, full: bool) { + if self.data.len() < Self::required_words(new_len) { + let default_value = if full { u64::MAX } else { 0 }; + self.data + .resize(Self::required_words(new_len), default_value); + } + let old_len = self.len; + self.len = new_len; + if old_len < new_len { + // TODO: optimize this loop to set whole words at a time + for i in old_len..new_len { + if full { + self.set(i); + } else { + self.clear(i); + } + } + } + } + + pub fn get(&self, bit: u32) -> bool { + assert!(bit < self.len); + let (index, bit_index) = Self::data_index_of(bit); + let group = self.data[index]; + group & U64GroupedBitmap::select_mask(bit_index) != 0 + } + + // Returns true iff the bit's group is all set + pub fn set(&mut self, bit: u32) -> bool { + assert!(bit < self.len); + let (index, bit_index) = Self::data_index_of(bit); + let mut group = self.data[index]; + group |= Self::select_mask(bit_index); + self.data[index] = group; + + group == u64::MAX + } + + pub fn clear(&mut self, bit: u32) { + assert!(bit < self.len, "{bit} must be less than {}", self.len); + let (index, bit_index) = Self::data_index_of(bit); + self.data[index] &= !Self::select_mask(bit_index); + } +} + +#[cfg(test)] +mod test { + use crate::tree_store::page_store::bitmap::BtreeBitmap; + use rand::prelude::IteratorRandom; + use rand::rngs::StdRng; + use rand::{RngExt, SeedableRng}; + use std::collections::HashSet; + + #[test] + fn alloc() { + let num_pages = 2; + let mut allocator = BtreeBitmap::new(num_pages, num_pages); + for i in 0..num_pages { + allocator.clear(i); + } + for i in 0..num_pages { + assert_eq!(i, allocator.alloc().unwrap()); + } + assert!(allocator.alloc().is_none()); + } + + #[test] + fn record_alloc() { + let mut allocator = BtreeBitmap::new(2, 2); + allocator.clear(0); + allocator.clear(1); + allocator.set(0); + assert_eq!(1, allocator.alloc().unwrap()); + assert!(allocator.alloc().is_none()); + } + + #[test] + fn free() { + let mut allocator = BtreeBitmap::new(1, 1); + allocator.clear(0); + assert_eq!(0, allocator.alloc().unwrap()); + assert!(allocator.alloc().is_none()); + allocator.clear(0); + assert_eq!(0, allocator.alloc().unwrap()); + } + + #[test] + fn reuse_lowest() { + let num_pages = 65; + let mut allocator = BtreeBitmap::new(num_pages, num_pages); + for i in 0..num_pages { + allocator.clear(i); + } + for i in 0..num_pages { + assert_eq!(i, allocator.alloc().unwrap()); + } + allocator.clear(5); + allocator.clear(15); + assert_eq!(5, allocator.alloc().unwrap()); + assert_eq!(15, allocator.alloc().unwrap()); + assert!(allocator.alloc().is_none()); + } + + #[test] + fn all_space_used() { + let num_pages = 65; + let mut allocator = BtreeBitmap::new(num_pages, num_pages); + for i in 0..num_pages { + allocator.clear(i); + } + // Allocate everything + while allocator.alloc().is_some() {} + // The last u64 must be used, since the leaf layer is compact + assert_eq!( + u64::MAX, + *allocator.heights.last().unwrap().data.last().unwrap() + ); + } + + #[test] + fn find_free() { + let num_pages = 129; + let mut allocator = BtreeBitmap::new(num_pages, num_pages); + assert!(allocator.find_first_unset().is_none()); + allocator.clear(128); + assert_eq!(allocator.find_first_unset().unwrap(), 128); + allocator.clear(65); + assert_eq!(allocator.find_first_unset().unwrap(), 65); + allocator.clear(8); + assert_eq!(allocator.find_first_unset().unwrap(), 8); + allocator.clear(0); + assert_eq!(allocator.find_first_unset().unwrap(), 0); + } + + #[test] + fn resize_beyond_initial_capacity() { + let mut bm = BtreeBitmap::new_padded(60, 60, 1_048_576); + let height = bm.heights.len(); + + bm.clear(0); + bm.clear(30); + bm.clear(59); + + bm.resize(5000, true); + + // Height is pre-padded at construction, stays the same + assert_eq!(bm.heights.len(), height); + + // Previously cleared bits survive the resize + assert!(!bm.get(0)); + assert!(!bm.get(30)); + assert!(!bm.get(59)); + + // Newly grown region should be marked full (true) + assert!(bm.get(60)); + assert!(bm.get(4999)); + + // Alloc should return the previously cleared entries + assert_eq!(bm.alloc(), Some(0)); + assert_eq!(bm.alloc(), Some(30)); + assert_eq!(bm.alloc(), Some(59)); + assert!(bm.alloc().is_none()); + + // Clear a bit in the newly grown area, alloc should find it + bm.clear(3000); + assert_eq!(bm.alloc(), Some(3000)); + assert!(bm.alloc().is_none()); + } + + #[test] + fn random_pattern() { + let seed = rand::rng().random(); + // Print the seed to debug for reproducibility, in case this test fails + println!("seed={seed}"); + let mut rng = StdRng::seed_from_u64(seed); + + let num_pages = rng.random_range(2..10000); + let mut allocator = BtreeBitmap::new(num_pages, num_pages); + for i in 0..num_pages { + allocator.clear(i); + } + let mut allocated = HashSet::new(); + + for _ in 0..(num_pages * 2) { + if rng.random_bool(0.75) { + if let Some(page) = allocator.alloc() { + allocated.insert(page); + } else { + assert_eq!(allocated.len(), num_pages as usize); + } + } else if let Some(to_free) = allocated.iter().choose(&mut rng).copied() { + allocator.clear(to_free); + allocated.remove(&to_free); + } + } + + for _ in allocated.len()..(num_pages as usize) { + allocator.alloc().unwrap(); + } + assert!(allocator.alloc().is_none()); + + for i in 0..num_pages { + allocator.clear(i); + } + + for _ in 0..num_pages { + allocator.alloc().unwrap(); + } + assert!(allocator.alloc().is_none()); + } +} diff --git a/v2/redb/src/tree_store/page_store/buddy_allocator.rs b/v2/redb/src/tree_store/page_store/buddy_allocator.rs new file mode 100644 index 000000000..db65c0470 --- /dev/null +++ b/v2/redb/src/tree_store/page_store/buddy_allocator.rs @@ -0,0 +1,540 @@ +use crate::tree_store::PageNumber; +use crate::tree_store::page_store::bitmap::BtreeBitmap; +use crate::tree_store::page_store::page_manager::MAX_MAX_PAGE_ORDER; +use std::cmp::min; +#[cfg(test)] +use std::collections::HashSet; +use std::mem::size_of; + +const MAX_ORDER_OFFSET: usize = 0; +const PADDING: usize = 3; +const NUM_PAGES_OFFSET: usize = MAX_ORDER_OFFSET + size_of::() + PADDING; +const FREE_END_OFFSETS: usize = NUM_PAGES_OFFSET + size_of::(); + +fn calculate_usable_order(pages: u32) -> u8 { + let max_order = (32 - pages.leading_zeros() - 1).try_into().unwrap(); + min(MAX_MAX_PAGE_ORDER, max_order) +} + +fn next_higher_order(page_number: u32) -> u32 { + page_number / 2 +} + +fn buddy_page(page_number: u32) -> u32 { + page_number ^ 1 +} + +// Handles allocation of dynamically sized pages, supports pages of up to page_size * 2^max_order bytes +// +// Pages are marked free at only a single order, and it must always be the largest order +pub(crate) struct BuddyAllocator { + free: Vec, + len: u32, + max_order: u8, +} + +impl BuddyAllocator { + pub(crate) fn new(num_pages: u32, max_page_capacity: u32) -> Self { + let max_order = calculate_usable_order(max_page_capacity); + + let mut capacity_for_order = max_page_capacity; + let mut pages_for_order = num_pages; + let mut free = vec![]; + for _ in 0..=max_order { + // Lazily size each bitmap for the actual current pages, but pad the tree + // height so resize() can grow to the full region capacity without needing + // to insert new tree levels. + free.push(BtreeBitmap::new_padded( + pages_for_order, + pages_for_order, + capacity_for_order, + )); + + pages_for_order = next_higher_order(pages_for_order); + capacity_for_order = next_higher_order(capacity_for_order); + } + + // Mark the available pages, starting with the highest order + let mut accounted_pages = 0; + for order in (0..=max_order).rev() { + let order_size = 2u32.pow(order.into()); + while accounted_pages + order_size <= num_pages { + let page = accounted_pages / order_size; + free[order as usize].clear(page); + accounted_pages += order_size; + } + } + assert_eq!(accounted_pages, num_pages); + + Self { + free, + len: num_pages, + max_order, + } + } + + pub(crate) fn xxh3_hash(&self) -> u128 { + let mut result = 0; + for x in &self.free { + result ^= x.xxh3_hash(); + } + + result + } + + // Data structure format: + // max_order: u8 + // padding: 3 bytes + // num_pages: u32 + // free_ends: array of u32, with ending offset for BtreeBitmap structure for the given order + // ... BtreeBitmap structures + pub(crate) fn to_vec(&self) -> Vec { + let serialized: Vec> = self.free.iter().map(BtreeBitmap::to_vec).collect(); + + let header_bytes = 1 + 3 + size_of::(); + let offsets_bytes = (self.max_order as usize + 1) * size_of::(); + let data_bytes: usize = serialized.iter().map(Vec::len).sum(); + + let mut result = Vec::with_capacity(header_bytes + offsets_bytes + data_bytes); + result.push(self.max_order); + result.extend([0u8; 3]); + result.extend(self.len.to_le_bytes()); + + let mut data_offset = header_bytes + offsets_bytes; + for bitmap in &serialized { + data_offset += bitmap.len(); + let offset_u32: u32 = data_offset.try_into().unwrap(); + result.extend(offset_u32.to_le_bytes()); + } + for bitmap in &serialized { + result.extend_from_slice(bitmap); + } + + result + } + + pub(crate) fn from_bytes(data: &[u8]) -> Self { + let max_order = data[MAX_ORDER_OFFSET]; + let num_pages = u32::from_le_bytes( + data[NUM_PAGES_OFFSET..(NUM_PAGES_OFFSET + size_of::())] + .try_into() + .unwrap(), + ); + + let mut metadata = FREE_END_OFFSETS; + let mut data_start = FREE_END_OFFSETS + (max_order as usize + 1) * size_of::(); + + let mut free = vec![]; + for _ in 0..=max_order { + let data_end = u32::from_le_bytes( + data[metadata..(metadata + size_of::())] + .try_into() + .unwrap(), + ) as usize; + free.push(BtreeBitmap::from_bytes(&data[data_start..data_end])); + data_start = data_end; + metadata += size_of::(); + } + + Self { + free, + len: num_pages, + max_order, + } + } + + #[inline] + pub(crate) fn highest_free_order(&self) -> Option { + (0..=self.max_order) + .rev() + .find(|order| self.get_order_free(*order).has_unset()) + } + + pub(crate) fn count_allocated_pages(&self) -> u32 { + self.len() - self.count_free_pages() + } + + pub(crate) fn count_free_pages(&self) -> u32 { + let mut pages = 0; + for order in 0..=self.max_order { + pages += self.get_order_free(order).count_unset() * 2u32.pow(order.into()); + } + pages + } + + pub(crate) fn get_max_order(&self) -> u8 { + self.max_order + } + + fn find_free_order(&self, mut page: u32) -> Option { + for order in 0..=self.max_order { + if page < self.get_order_free(order).len() && !self.get_order_free(order).get(page) { + return Some(order); + } + page = next_higher_order(page); + } + None + } + + pub(crate) fn trailing_free_pages(&self) -> u32 { + let mut free_pages = 0; + let mut next_page = self.len() - 1; + while let Some(order) = self.find_free_order(next_page) { + let order_size = 2u32.pow(order.into()); + free_pages += order_size; + if order_size > next_page { + break; + } + next_page -= order_size; + } + + free_pages + } + + #[cfg_attr(not(debug_assertions), expect(dead_code))] + #[cfg_attr(not(test), expect(clippy::unused_self))] + #[cfg_attr(not(test), expect(unused_variables))] + pub(crate) fn check_allocated_pages(&self, region: u32, allocated_pages: &[PageNumber]) { + #[cfg(test)] + // Check the result against the free index to be sure it matches + { + let mut free_check = HashSet::new(); + for i in 0..self.len() { + if self.find_free_order(i).is_none() { + free_check.insert(PageNumber::new(region, i, 0)); + } + } + + let mut check_result = HashSet::new(); + for page in allocated_pages { + check_result.extend(page.to_order0()); + } + assert_eq!(free_check, check_result); + } + } + + pub(crate) fn len(&self) -> u32 { + self.len + } + + pub(crate) fn resize(&mut self, new_size: u32) { + self.debug_check_consistency(); + if new_size > self.len() { + let mut processed_pages = self.len(); + let mut pages_for_order = new_size; + for order in &mut self.free { + order.resize(pages_for_order, true); + pages_for_order = next_higher_order(pages_for_order); + } + // Align to the highest order possible + while processed_pages < new_size { + let order: u8 = processed_pages.trailing_zeros().try_into().unwrap(); + let order_size = 2u32.pow(order.into()); + let page = processed_pages / order_size; + debug_assert_eq!(processed_pages % order_size, 0); + if order >= self.max_order || processed_pages + order_size > new_size { + break; + } + self.free_inner(page, order); + processed_pages += order_size; + } + // Allocate the remaining space, at the highest order + for order in (0..=self.max_order).rev() { + let order_size = 2u32.pow(order.into()); + while processed_pages + order_size <= new_size { + let page = processed_pages / order_size; + self.free_inner(page, order); + processed_pages += order_size; + } + } + assert_eq!(processed_pages, new_size); + } else { + let mut processed_pages = new_size; + // Align to the highest order possible + while processed_pages < self.len() { + let order: u8 = processed_pages.trailing_zeros().try_into().unwrap(); + if order >= self.max_order { + break; + } + let order_size = 2u32.pow(order.into()); + let page = processed_pages / order_size; + debug_assert_eq!(processed_pages % order_size, 0); + if processed_pages + order_size > self.len() { + break; + } + self.record_alloc_inner(page, order); + processed_pages += order_size; + } + // Allocate the remaining space, at the highest order + for order in (0..=self.max_order).rev() { + let order_size = 2u32.pow(order.into()); + while processed_pages + order_size <= self.len() { + let page = processed_pages / order_size; + self.record_alloc_inner(page, order); + processed_pages += order_size; + } + } + assert_eq!(processed_pages, self.len()); + let mut pages_for_order = new_size; + for order in &mut self.free { + order.resize(pages_for_order, true); + pages_for_order = next_higher_order(pages_for_order); + } + } + self.len = new_size; + self.debug_check_consistency(); + } + + #[allow(unused_variables)] + fn debug_check_consistency(&self) { + // Don't enable when fuzzing, because this is kind of expensive + #[cfg(all(debug_assertions, not(fuzzing)))] + { + let mut processed = 0; + // Ensure that no page is free at multiple orders + while processed < self.len() { + let mut found = false; + let mut page = processed; + for order in 0..=self.max_order { + let allocator = &self.free[order as usize]; + if allocator.len() <= page { + break; + } + if !allocator.get(page) { + assert!(!found); + found = true; + } + page = next_higher_order(page); + } + processed += 1; + } + + // Ensure that all buddy pages are merged, except at the highest order + for order in (0..self.max_order).rev() { + let order_len = self.len() / (2u32.pow(order.into())); + let allocator = &self.free[order as usize]; + for page in 0..order_len { + if !allocator.get(page) { + let buddy = buddy_page(page); + if buddy < allocator.len() { + let buddy_allocated = allocator.get(buddy); + assert!(buddy_allocated, "order={order} page={page} buddy={buddy}",); + } + } + } + } + } + } + + // Allocates a page of the given order at the lowest index possible. Will split a higher order page, + // to get one of lower index + pub(crate) fn alloc_lowest(&mut self, order: u8) -> Option { + // Lowest index at the requested order + let mut best_index_at_order = self.alloc_inner(order)?; + // Best (index, order) found + let mut best = (best_index_at_order, order); + + // Find the lowest index at which we can fill this allocation, across all orders + let mut multiplier = 2; + for i in (order + 1)..=self.max_order { + if let Some(index) = self.alloc_inner(i) { + let index_at_order = index * multiplier; + if index_at_order < best_index_at_order { + self.free_inner(best.0, best.1); + best_index_at_order = index_at_order; + best = (index, i); + } else { + self.free_inner(index, i); + } + } + multiplier *= 2; + } + + // Split the page, until we get to the requested order + while best.1 > order { + let (best_index, best_order) = best; + let (free1, free2) = (best_index * 2, best_index * 2 + 1); + let allocator = self.get_order_free_mut(best_order - 1); + debug_assert!(allocator.get(free1)); + debug_assert!(allocator.get(free2)); + allocator.clear(free2); + best = (free1, best_order - 1); + } + assert_eq!(best.0, best_index_at_order); + + Some(best.0) + } + + pub(crate) fn alloc(&mut self, order: u8) -> Option { + self.alloc_inner(order) + } + + pub(crate) fn alloc_inner(&mut self, order: u8) -> Option { + if order > self.max_order { + return None; + } + let allocator = self.get_order_free_mut(order); + if let Some(x) = allocator.alloc() { + Some(x) + } else { + // Try to allocate a higher order page and split it + let upper_page = self.alloc_inner(order + 1)?; + let (free1, free2) = (upper_page * 2, upper_page * 2 + 1); + let allocator = self.get_order_free_mut(order); + debug_assert!(allocator.get(free1)); + debug_assert!(allocator.get(free2)); + allocator.clear(free2); + + Some(free1) + } + } + + // `page_number` must be free + pub(crate) fn record_alloc(&mut self, page_number: u32, order: u8) { + assert!(order <= self.max_order); + // Split parent pages as necessary, and update the free index + self.record_alloc_inner(page_number, order); + } + + pub(crate) fn record_alloc_inner(&mut self, page_number: u32, order: u8) { + let allocator = self.get_order_free_mut(order); + if allocator.get(page_number) { + // Need to split parent page + let upper_page = next_higher_order(page_number); + self.record_alloc_inner(upper_page, order + 1); + let allocator = self.get_order_free_mut(order); + + let (free1, free2) = (upper_page * 2, upper_page * 2 + 1); + debug_assert!(free1 == page_number || free2 == page_number); + if free1 == page_number { + allocator.clear(free2); + } else { + allocator.clear(free1); + } + } else { + allocator.set(page_number); + } + } + + /// data must have been initialized by `Self::init_new()` + pub(crate) fn free(&mut self, page_number: u32, order: u8) { + debug_assert!(self.get_order_free_mut(order).get(page_number)); + + // Update the free index and merge free pages + self.free_inner(page_number, order); + } + + pub(crate) fn free_inner(&mut self, page_number: u32, order: u8) { + if order == self.max_order { + let allocator = self.get_order_free_mut(order); + allocator.clear(page_number); + return; + } + + let allocator = self.get_order_free_mut(order); + let buddy = buddy_page(page_number); + if buddy >= allocator.len() || allocator.get(buddy) { + allocator.clear(page_number); + } else { + // Merge into higher order page + allocator.set(buddy); + self.free_inner(next_higher_order(page_number), order + 1); + } + } + + fn get_order_free_mut(&mut self, order: u8) -> &mut BtreeBitmap { + &mut self.free[order as usize] + } + + fn get_order_free(&self, order: u8) -> &BtreeBitmap { + &self.free[order as usize] + } +} + +#[cfg(test)] +mod test { + use crate::tree_store::page_store::buddy_allocator::BuddyAllocator; + + #[test] + fn record_alloc_buddy() { + let num_pages = 256; + let mut allocator = BuddyAllocator::new(num_pages, num_pages); + assert_eq!(allocator.count_allocated_pages(), 0); + + for page in 0..num_pages { + allocator.record_alloc(page, 0); + } + assert_eq!(allocator.count_allocated_pages(), num_pages); + + assert!(allocator.alloc(0).is_none()); + + for page in 0..num_pages { + allocator.free(page, 0); + } + assert_eq!(allocator.count_allocated_pages(), 0); + } + + #[test] + fn buddy_merge() { + let num_pages = 256; + let mut allocator = BuddyAllocator::new(num_pages, num_pages); + assert_eq!(allocator.count_allocated_pages(), 0); + + for _ in 0..num_pages { + allocator.alloc(0).unwrap(); + } + for page in 0..num_pages { + allocator.free(page, 0); + } + assert_eq!(allocator.count_allocated_pages(), 0); + + // Test that everything got merged back together, so that we fill order 7 allocations + for _ in 0..(num_pages / 2u32.pow(7)) { + allocator.alloc(7).unwrap(); + } + } + + #[test] + fn alloc_large() { + let num_pages = 256; + let max_order = 7; + let mut allocator = BuddyAllocator::new(num_pages, num_pages); + assert_eq!(allocator.count_allocated_pages(), 0); + + let mut allocated = vec![]; + for order in 0..=max_order { + allocated.push((allocator.alloc(order).unwrap(), order)); + } + assert_eq!(allocator.count_allocated_pages(), num_pages - 1); + + for order in 1..=max_order { + assert!(allocator.alloc(order).is_none()); + } + + for (page, order) in allocated { + allocator.free(page, order); + } + assert_eq!(allocator.count_allocated_pages(), 0); + } + + #[test] + fn serialized_size() { + // Check that serialized size is as expected for a full region + let max_region_pages = 1024 * 1024; + let allocator = BuddyAllocator::new(max_region_pages, max_region_pages); + let max_region_pages = u64::from(max_region_pages); + // 2x because that's the integral of 1/2^x to account for all the 21 orders + let allocated_state_bits = 2 * max_region_pages; + // + u32 * 21 because of the length field + let allocated_state_bytes = allocated_state_bits / 8 + 4 * 21; + + // Add 2/64 to account for the intermediate index of the bitmap tree + let free_state_bits = allocated_state_bits + allocated_state_bits * 2 / 64; + // Add u32s to account for the stored offsets. Might be a bit of an over estimate + let free_state_bytes = free_state_bits / 8 + 4 * 21 * 3 + 21 * 4; + // BuddyAllocator overhead + let buddy_state_bytes = free_state_bytes + allocated_state_bytes + 21 * 2 * 4 + 8; + + assert!((allocator.to_vec().len() as u64) <= buddy_state_bytes); + } +} diff --git a/v2/redb/src/tree_store/page_store/cached_file.rs b/v2/redb/src/tree_store/page_store/cached_file.rs new file mode 100644 index 000000000..110ca8ec6 --- /dev/null +++ b/v2/redb/src/tree_store/page_store/cached_file.rs @@ -0,0 +1,656 @@ +use crate::tree_store::page_store::base::PageHint; +use crate::tree_store::page_store::lru_cache::LRUCache; +use crate::{CacheStats, DatabaseError, Result, StorageBackend, StorageError}; +use std::ops::{Index, IndexMut}; +use std::slice::SliceIndex; +#[cfg(feature = "cache_metrics")] +use std::sync::atomic::AtomicU64; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex, MutexGuard, RwLock}; + +// Allocates an `Arc<[u8]>` in one step. `Arc::<[u8]>::from(vec![0; len])` would +// allocate the Vec and then allocate a new Arc and memcpy into it. +fn zero_filled_arc(len: usize) -> Arc<[u8]> { + // This is documented to do a single allocation: https://doc.rust-lang.org/std/sync/struct.Arc.html#iterators-of-known-length + std::iter::repeat_n(0u8, len).collect() +} + +pub(super) struct WritablePage { + buffer: Arc>, + offset: u64, + data: Arc<[u8]>, +} + +impl WritablePage { + pub(super) fn mem(&self) -> &[u8] { + &self.data + } + + pub(super) fn mem_mut(&mut self) -> &mut [u8] { + Arc::get_mut(&mut self.data).unwrap() + } +} + +impl Drop for WritablePage { + fn drop(&mut self) { + self.buffer + .lock() + .unwrap() + .return_value(self.offset, self.data.clone()); + } +} + +impl> Index for WritablePage { + type Output = I::Output; + + fn index(&self, index: I) -> &Self::Output { + self.mem().index(index) + } +} + +impl> IndexMut for WritablePage { + fn index_mut(&mut self, index: I) -> &mut Self::Output { + self.mem_mut().index_mut(index) + } +} + +#[derive(Default)] +struct LRUWriteCache { + cache: LRUCache>>, +} + +impl LRUWriteCache { + fn new() -> Self { + Self { + cache: Default::default(), + } + } + + fn insert(&mut self, key: u64, value: Arc<[u8]>) { + assert!(self.cache.insert(key, Some(value)).is_none()); + } + + fn get(&self, key: u64) -> Option<&Arc<[u8]>> { + self.cache.get(key).map(|x| x.as_ref().unwrap()) + } + + fn remove(&mut self, key: u64) -> Option> { + if let Some(value) = self.cache.remove(key) { + assert!(value.is_some()); + return value; + } + None + } + + fn return_value(&mut self, key: u64, value: Arc<[u8]>) { + assert!(self.cache.get_mut(key).unwrap().replace(value).is_none()); + } + + fn take_value(&mut self, key: u64) -> Option> { + if let Some(value) = self.cache.get_mut(key) { + let result = value.take().unwrap(); + return Some(result); + } + None + } + + fn pop_lowest_priority(&mut self) -> Option<(u64, Arc<[u8]>)> { + for _ in 0..self.cache.len() { + if let Some((k, v)) = self.cache.pop_lowest_priority() { + if let Some(v_inner) = v { + return Some((k, v_inner)); + } + + // Value is borrowed by take_value(). We can't evict it, so put it back. + self.cache.insert(k, v); + } else { + break; + } + } + None + } + + fn clear(&mut self) { + self.cache.clear(); + } +} + +#[derive(Debug)] +struct CheckedBackend { + file: Box, + io_failed: AtomicBool, + closed: AtomicBool, +} + +impl CheckedBackend { + fn new(file: Box) -> Self { + Self { + file, + io_failed: AtomicBool::new(false), + closed: AtomicBool::new(false), + } + } + + fn check_failure(&self) -> Result<()> { + if self.io_failed.load(Ordering::Acquire) { + if self.closed.load(Ordering::Acquire) { + Err(StorageError::DatabaseClosed) + } else { + Err(StorageError::PreviousIo) + } + } else { + Ok(()) + } + } + + fn close(&self) -> Result { + self.closed.store(true, Ordering::Release); + self.io_failed.store(true, Ordering::Release); + self.file.close()?; + + Ok(()) + } + + fn len(&self) -> Result { + self.check_failure()?; + let result = self.file.len(); + if result.is_err() { + self.io_failed.store(true, Ordering::Release); + } + result.map_err(StorageError::from) + } + + fn read(&self, offset: u64, out: &mut [u8]) -> Result<()> { + self.check_failure()?; + let result = self.file.read(offset, out); + if result.is_err() { + self.io_failed.store(true, Ordering::Release); + } + result.map_err(StorageError::from) + } + + fn set_len(&self, len: u64) -> Result<()> { + self.check_failure()?; + let result = self.file.set_len(len); + if result.is_err() { + self.io_failed.store(true, Ordering::Release); + } + result.map_err(StorageError::from) + } + + fn sync_data(&self) -> Result<()> { + self.check_failure()?; + let result = self.file.sync_data(); + if result.is_err() { + self.io_failed.store(true, Ordering::Release); + } + result.map_err(StorageError::from) + } + + fn write(&self, offset: u64, data: &[u8]) -> Result<()> { + self.check_failure()?; + let result = self.file.write(offset, data); + if result.is_err() { + self.io_failed.store(true, Ordering::Release); + } + result.map_err(StorageError::from) + } +} + +pub(super) struct PagedCachedFile { + file: CheckedBackend, + page_size: u64, + // Dynamic cache partitioning. Three invariants: + // + // 1. The write buffer NEVER exceeds 50% of max_cache_size. + // Pages beyond this limit are flushed to disk immediately. + // 2. The write buffer evicts from the read cache only when + // write < 50% AND read > 50% (fairness). + // 3. write + read never exceeds max_cache_size. + // + // Together these guarantee that the read cache can grow up to 100% when no + // writes are in progress, while write-heavy workloads never starve readers + // below 50%. + // + // We track usage with two atomic counters and compute the total on the fly. + // The resulting read is not perfectly atomic (between loading the two + // counters a concurrent operation could change one), but the budget is a + // soft limit and momentary over-/under-counting by one page is harmless. + // A third "total" counter would add contention on every insert/remove for + // negligible accuracy gain. + read_cache_bytes: AtomicUsize, + write_buffer_bytes: AtomicUsize, + max_cache_size: usize, + // Rotates the starting stripe for read-cache eviction + next_eviction_stripe: AtomicUsize, + #[cfg(feature = "cache_metrics")] + reads_total: AtomicU64, + #[cfg(feature = "cache_metrics")] + reads_hits: AtomicU64, + #[cfg(feature = "cache_metrics")] + writes_total: AtomicU64, + #[cfg(feature = "cache_metrics")] + writes_hits: AtomicU64, + #[cfg(feature = "cache_metrics")] + evictions: AtomicU64, + read_cache: Vec>>>, + // TODO: maybe move this cache to WriteTransaction? + write_buffer: Arc>, +} + +impl PagedCachedFile { + pub(super) fn new( + file: Box, + page_size: u64, + max_cache_size: usize, + ) -> Result { + let read_cache = (0..Self::lock_stripes()) + .map(|_| RwLock::new(LRUCache::new())) + .collect(); + + Ok(Self { + file: CheckedBackend::new(file), + page_size, + read_cache_bytes: AtomicUsize::new(0), + write_buffer_bytes: AtomicUsize::new(0), + max_cache_size, + next_eviction_stripe: AtomicUsize::new(0), + #[cfg(feature = "cache_metrics")] + reads_total: Default::default(), + #[cfg(feature = "cache_metrics")] + reads_hits: Default::default(), + #[cfg(feature = "cache_metrics")] + writes_total: Default::default(), + #[cfg(feature = "cache_metrics")] + writes_hits: Default::default(), + #[cfg(feature = "cache_metrics")] + evictions: Default::default(), + read_cache, + write_buffer: Arc::new(Mutex::new(LRUWriteCache::new())), + }) + } + + #[allow(clippy::unused_self)] + pub(crate) fn cache_stats(&self) -> CacheStats { + #[cfg(not(feature = "cache_metrics"))] + { + CacheStats { + evictions: 0, + read_hits: 0, + read_misses: 0, + write_hits: 0, + write_misses: 0, + used_bytes: 0, + } + } + + #[cfg(feature = "cache_metrics")] + { + let read_hits = self.reads_hits.load(Ordering::Acquire); + let read_total = self.reads_total.load(Ordering::Acquire); + let write_hits = self.writes_hits.load(Ordering::Acquire); + let write_total = self.writes_total.load(Ordering::Acquire); + let read_bytes = self.read_cache_bytes.load(Ordering::Acquire); + let write_bytes = self.write_buffer_bytes.load(Ordering::Acquire); + CacheStats { + evictions: self.evictions.load(Ordering::Acquire), + read_hits, + read_misses: read_total - read_hits, + write_hits, + write_misses: write_total - write_hits, + used_bytes: read_bytes + write_bytes, + } + } + } + + pub(crate) fn close(&self) -> Result { + self.file.close() + } + + pub(crate) fn check_io_errors(&self) -> Result { + self.file.check_failure() + } + + pub(crate) fn raw_file_len(&self) -> Result { + self.file.len() + } + + const fn lock_stripes() -> u64 { + 131 + } + + // Evict entries from the read cache to free at least `bytes_needed` bytes. + // Iterates through cache stripes and pops lowest-priority entries. + // + // Caller must hold the write_buffer mutex to maintain the lock ordering + // invariant (write_buffer lock is always acquired before read_cache locks). + fn evict_from_read_cache( + &self, + bytes_needed: usize, + _write_lock: &MutexGuard<'_, LRUWriteCache>, + ) { + let num_stripes = self.read_cache.len(); + let start = self.next_eviction_stripe.fetch_add(1, Ordering::Relaxed) % num_stripes; + let mut freed = 0; + for i in 0..num_stripes { + if freed >= bytes_needed { + break; + } + let stripe = (start + i) % num_stripes; + let mut lock = self.read_cache[stripe].write().unwrap(); + while freed < bytes_needed { + if let Some((_, v)) = lock.pop_lowest_priority() { + #[cfg(feature = "cache_metrics")] + { + self.evictions.fetch_add(1, Ordering::Relaxed); + } + freed += v.len(); + self.read_cache_bytes.fetch_sub(v.len(), Ordering::AcqRel); + } else { + break; + } + } + } + } + + fn flush_write_buffer(&self) -> Result { + let mut write_buffer = self.write_buffer.lock().unwrap(); + + for (offset, buffer) in write_buffer.cache.iter() { + self.file.write(*offset, buffer.as_ref().unwrap())?; + } + // Transfer flushed pages into the read cache so they are available + // for subsequent reads without a file I/O. The write buffer is being + // drained, so the total check only considers the read cache size. + for (offset, buffer) in write_buffer.cache.iter_mut() { + let buffer = buffer.take().unwrap(); + let cache_size = self + .read_cache_bytes + .fetch_add(buffer.len(), Ordering::AcqRel); + + if cache_size + buffer.len() <= self.max_cache_size { + let cache_slot: usize = (offset % Self::lock_stripes()).try_into().unwrap(); + let mut lock = self.read_cache[cache_slot].write().unwrap(); + if let Some(replaced) = lock.insert(*offset, buffer) { + // A race could cause us to replace an existing buffer + self.read_cache_bytes + .fetch_sub(replaced.len(), Ordering::AcqRel); + } + } else { + self.read_cache_bytes + .fetch_sub(buffer.len(), Ordering::AcqRel); + break; + } + } + self.write_buffer_bytes.store(0, Ordering::Release); + write_buffer.clear(); + + Ok(()) + } + + // Caller should invalidate all cached pages that are no longer valid + pub(super) fn resize(&self, len: u64) -> Result { + // TODO: be more fine-grained about this invalidation + self.invalidate_cache_all(); + + self.file.set_len(len) + } + + pub(super) fn flush(&self) -> Result { + self.flush_write_buffer()?; + + self.file.sync_data() + } + + // Make writes visible to readers, but does not guarantee any durability + pub(super) fn write_barrier(&self) -> Result { + // TODO: non-durable commits would be much faster, if this did not issues writes to disk, + // and instead just made the data visible to readers + self.flush_write_buffer() + } + + // Read directly from the file, ignoring any cached data + pub(super) fn read_direct(&self, offset: u64, len: usize) -> Result> { + let mut buffer = vec![0; len]; + self.file.read(offset, &mut buffer)?; + Ok(buffer) + } + + // Like `read_direct`, but writes directly into an `Arc<[u8]>` instead of a + // `Vec` that is then copied into an `Arc`. The buffer is zero-filled + // because `StorageBackend::read` takes `&mut [u8]`. + fn read_direct_into_arc(&self, offset: u64, len: usize) -> Result> { + let mut arc = zero_filled_arc(len); + self.file.read(offset, Arc::get_mut(&mut arc).unwrap())?; + Ok(arc) + } + + // Read with caching. Caller must not read overlapping ranges without first calling invalidate_cache(). + // Doing so will not cause UB, but is a logic error. + pub(super) fn read(&self, offset: u64, len: usize, hint: PageHint) -> Result> { + debug_assert_eq!(0, offset % self.page_size); + #[cfg(feature = "cache_metrics")] + self.reads_total.fetch_add(1, Ordering::AcqRel); + + if !matches!(hint, PageHint::Clean) { + let lock = self.write_buffer.lock().unwrap(); + if let Some(cached) = lock.get(offset) { + #[cfg(feature = "cache_metrics")] + self.reads_hits.fetch_add(1, Ordering::Release); + debug_assert_eq!(cached.len(), len); + return Ok(cached.clone()); + } + } + + let cache_slot: usize = (offset % Self::lock_stripes()).try_into().unwrap(); + { + let read_lock = self.read_cache[cache_slot].read().unwrap(); + if let Some(cached) = read_lock.get(offset) { + #[cfg(feature = "cache_metrics")] + self.reads_hits.fetch_add(1, Ordering::Release); + debug_assert_eq!(cached.len(), len); + return Ok(cached.clone()); + } + } + + let buffer = self.read_direct_into_arc(offset, len)?; + let cache_size = self.read_cache_bytes.fetch_add(len, Ordering::AcqRel); + let mut write_lock = self.read_cache[cache_slot].write().unwrap(); + let cache_size = if let Some(replaced) = write_lock.insert(offset, buffer.clone()) { + // A race could cause us to replace an existing buffer + self.read_cache_bytes + .fetch_sub(replaced.len(), Ordering::AcqRel) + } else { + cache_size + }; + + // Rule 3: evict from this read-cache slot if the total exceeds the + // budget. We evict exactly `len` bytes (one page) per miss to avoid + // over-eviction spikes. + let write_bytes = self.write_buffer_bytes.load(Ordering::Acquire); + let over_total = cache_size + len + write_bytes > self.max_cache_size; + let mut removed = 0; + if over_total { + while removed < len { + if let Some((_, v)) = write_lock.pop_lowest_priority() { + #[cfg(feature = "cache_metrics")] + { + self.evictions.fetch_add(1, Ordering::Relaxed); + } + removed += v.len(); + } else { + break; + } + } + } + if removed > 0 { + self.read_cache_bytes.fetch_sub(removed, Ordering::AcqRel); + } + + Ok(buffer) + } + + // Discard pending writes to the given range + pub(super) fn cancel_pending_write(&self, offset: u64, _len: usize) { + assert_eq!(0, offset % self.page_size); + if let Some(removed) = self.write_buffer.lock().unwrap().remove(offset) { + self.write_buffer_bytes + .fetch_sub(removed.len(), Ordering::Release); + } + } + + // Invalidate any caching of the given range. After this call overlapping reads of the range are allowed + // + // NOTE: Invalidating a cached region in subsections is permitted, as long as all subsections are invalidated + pub(super) fn invalidate_cache(&self, offset: u64, len: usize) { + let cache_slot: usize = (offset % Self::lock_stripes()).try_into().unwrap(); + let mut lock = self.read_cache[cache_slot].write().unwrap(); + if let Some(removed) = lock.remove(offset) { + assert_eq!(len, removed.len()); + self.read_cache_bytes + .fetch_sub(removed.len(), Ordering::AcqRel); + } + } + + pub(super) fn invalidate_cache_all(&self) { + for cache_slot in 0..self.read_cache.len() { + let mut lock = self.read_cache[cache_slot].write().unwrap(); + while let Some((_, removed)) = lock.pop_lowest_priority() { + self.read_cache_bytes + .fetch_sub(removed.len(), Ordering::AcqRel); + } + } + } + + // If overwrite is true, the page is initialized to zero + // cache_policy takes the existing data as an argument and returns the priority. The priority should be stable and not change after WritablePage is dropped + pub(super) fn write(&self, offset: u64, len: usize, overwrite: bool) -> Result { + assert_eq!(0, offset % self.page_size); + let mut lock = self.write_buffer.lock().unwrap(); + + // TODO: allow hint that page is known to be dirty and will not be in the read cache + let cache_slot: usize = (offset % Self::lock_stripes()).try_into().unwrap(); + let existing = { + let mut lock = self.read_cache[cache_slot].write().unwrap(); + if let Some(removed) = lock.remove(offset) { + assert_eq!( + len, + removed.len(), + "cache inconsistency {len} != {} for offset {offset}", + removed.len() + ); + self.read_cache_bytes + .fetch_sub(removed.len(), Ordering::AcqRel); + Some(removed) + } else { + None + } + }; + + let data = if let Some(removed) = lock.take_value(offset) { + #[cfg(feature = "cache_metrics")] + self.writes_hits.fetch_add(1, Ordering::AcqRel); + removed + } else { + let previous = self.write_buffer_bytes.fetch_add(len, Ordering::AcqRel); + let mut write_bytes = previous + len; + let half = self.max_cache_size / 2; + + // Rule 1: write buffer NEVER exceeds 50%. Flush excess to disk. + if write_bytes > half { + let excess = write_bytes - half; + let mut flushed = 0; + while flushed < excess { + if let Some((offset, buffer)) = lock.pop_lowest_priority() { + let removed_len = buffer.len(); + let result = self.file.write(offset, &buffer); + if result.is_err() { + lock.insert(offset, buffer); + } + result?; + self.write_buffer_bytes + .fetch_sub(removed_len, Ordering::Release); + #[cfg(feature = "cache_metrics")] + { + self.evictions.fetch_add(1, Ordering::Relaxed); + } + flushed += removed_len; + } else { + break; + } + } + write_bytes -= flushed; + } + + // Rules 2 + 3: after rule 1, write <= 50%. If the total still + // exceeds the budget then read must be > 50%, so evict from the + // read cache (fairness: we only take from read when read > 50%). + let read_bytes = self.read_cache_bytes.load(Ordering::Acquire); + if write_bytes + read_bytes > self.max_cache_size { + self.evict_from_read_cache(write_bytes + read_bytes - self.max_cache_size, &lock); + } + let result = if let Some(data) = existing { + #[cfg(feature = "cache_metrics")] + self.writes_hits.fetch_add(1, Ordering::AcqRel); + data + } else if overwrite { + #[cfg(feature = "cache_metrics")] + self.writes_hits.fetch_add(1, Ordering::AcqRel); + zero_filled_arc(len) + } else { + self.read_direct_into_arc(offset, len)? + }; + lock.insert(offset, result); + lock.take_value(offset).unwrap() + }; + #[cfg(feature = "cache_metrics")] + self.writes_total.fetch_add(1, Ordering::AcqRel); + Ok(WritablePage { + buffer: self.write_buffer.clone(), + offset, + data, + }) + } +} + +#[cfg(test)] +mod test { + use crate::StorageBackend; + use crate::backends::InMemoryBackend; + use crate::tree_store::PageHint; + use crate::tree_store::page_store::cached_file::PagedCachedFile; + use std::sync::Arc; + use std::sync::atomic::Ordering; + + #[test] + fn cache_leak() { + let backend = InMemoryBackend::new(); + backend.set_len(1024).unwrap(); + let cached_file = PagedCachedFile::new(Box::new(backend), 128, 1024).unwrap(); + let cached_file = Arc::new(cached_file); + + let t1 = { + let cached_file = cached_file.clone(); + std::thread::spawn(move || { + for _ in 0..1000 { + cached_file.read(0, 128, PageHint::None).unwrap(); + cached_file.invalidate_cache(0, 128); + } + }) + }; + let t2 = { + let cached_file = cached_file.clone(); + std::thread::spawn(move || { + for _ in 0..1000 { + cached_file.read(0, 128, PageHint::None).unwrap(); + cached_file.invalidate_cache(0, 128); + } + }) + }; + + t1.join().unwrap(); + t2.join().unwrap(); + cached_file.invalidate_cache(0, 128); + assert_eq!(cached_file.read_cache_bytes.load(Ordering::Acquire), 0); + } +} diff --git a/v2/redb/src/tree_store/page_store/fast_hash.rs b/v2/redb/src/tree_store/page_store/fast_hash.rs new file mode 100644 index 000000000..a8332982d --- /dev/null +++ b/v2/redb/src/tree_store/page_store/fast_hash.rs @@ -0,0 +1,34 @@ +use crate::tree_store::PageNumber; +use std::collections::{HashMap, HashSet}; +use std::hash::{BuildHasherDefault, Hasher}; + +// See "Computationally easy, spectrally good multipliers for congruential pseudorandom number generators" by Steele & Vigna +const K: u64 = 0xf135_7aea_2e62_a9c5; + +pub(crate) type FastHashMapU64 = HashMap>; +pub(crate) type PageNumberHashSet = HashSet>; + +#[derive(Copy, Clone, Default, Eq, PartialEq)] +pub(crate) struct FastHasher64 { + hash: u64, +} + +impl Hasher for FastHasher64 { + fn finish(&self) -> u64 { + #[cfg(target_pointer_width = "64")] + const ROTATE: u32 = 26; + #[cfg(target_pointer_width = "32")] + const ROTATE: u32 = 15; + + self.hash.rotate_left(ROTATE) + } + + fn write(&mut self, _bytes: &[u8]) { + unreachable!("Only hashing 8 bytes is supported"); + } + + fn write_u64(&mut self, x: u64) { + debug_assert_eq!(self.hash, 0); + self.hash = x.wrapping_mul(K); + } +} diff --git a/v2/redb/src/tree_store/page_store/file_backend/fallback.rs b/v2/redb/src/tree_store/page_store/file_backend/fallback.rs new file mode 100644 index 000000000..765c2be99 --- /dev/null +++ b/v2/redb/src/tree_store/page_store/file_backend/fallback.rs @@ -0,0 +1,51 @@ +use crate::{DatabaseError, Result, StorageBackend}; +use std::fs::File; +use std::io; +use std::io::{Read, Seek, SeekFrom, Write}; +use std::sync::Mutex; + +/// Stores a database as a file on-disk. +#[derive(Debug)] +pub struct FileBackend { + file: Mutex, +} + +impl FileBackend { + /// Creates a new backend which stores data to the given file. + pub fn new(file: File) -> Result { + Self::new_internal(file, false) + } + + pub(crate) fn new_internal(file: File, _: bool) -> Result { + Ok(Self { + file: Mutex::new(file), + }) + } +} + +impl StorageBackend for FileBackend { + fn len(&self) -> Result { + Ok(self.file.lock().unwrap().metadata()?.len()) + } + + fn read(&self, offset: u64, out: &mut [u8]) -> Result<(), io::Error> { + let mut file = self.file.lock().unwrap(); + file.seek(SeekFrom::Start(offset))?; + file.read_exact(out)?; + Ok(()) + } + + fn set_len(&self, len: u64) -> Result<(), io::Error> { + self.file.lock().unwrap().set_len(len) + } + + fn sync_data(&self) -> Result<(), io::Error> { + self.file.lock().unwrap().sync_data() + } + + fn write(&self, offset: u64, data: &[u8]) -> Result<(), io::Error> { + let mut file = self.file.lock().unwrap(); + file.seek(SeekFrom::Start(offset))?; + file.write_all(data) + } +} diff --git a/v2/redb/src/tree_store/page_store/file_backend/mod.rs b/v2/redb/src/tree_store/page_store/file_backend/mod.rs new file mode 100644 index 000000000..dd67d15b4 --- /dev/null +++ b/v2/redb/src/tree_store/page_store/file_backend/mod.rs @@ -0,0 +1,9 @@ +#[cfg(any(windows, unix, target_os = "wasi"))] +mod optimized; +#[cfg(any(windows, unix, target_os = "wasi"))] +pub use optimized::FileBackend; + +#[cfg(not(any(windows, unix, target_os = "wasi")))] +mod fallback; +#[cfg(not(any(windows, unix, target_os = "wasi")))] +pub use fallback::FileBackend; diff --git a/v2/redb/src/tree_store/page_store/file_backend/optimized.rs b/v2/redb/src/tree_store/page_store/file_backend/optimized.rs new file mode 100644 index 000000000..4c135bd9e --- /dev/null +++ b/v2/redb/src/tree_store/page_store/file_backend/optimized.rs @@ -0,0 +1,191 @@ +use crate::{DatabaseError, Result, StorageBackend}; +use std::fs::{File, TryLockError}; +use std::io; + +#[cfg(feature = "logging")] +use log::warn; + +#[cfg(unix)] +use std::os::unix::fs::FileExt; + +#[cfg(windows)] +use std::os::windows::fs::FileExt; + +/// Stores a database as a file on-disk. +#[derive(Debug)] +pub struct FileBackend { + lock_supported: bool, + file: File, +} + +impl FileBackend { + /// Creates a new backend which stores data to the given file. + pub fn new(file: File) -> Result { + Self::new_internal(file, false) + } + + pub(crate) fn new_internal(file: File, read_only: bool) -> Result { + let result = if read_only { + file.try_lock_shared() + } else { + file.try_lock() + }; + + match result { + Ok(()) => Ok(Self { + file, + lock_supported: true, + }), + Err(TryLockError::WouldBlock) => Err(DatabaseError::DatabaseAlreadyOpen), + Err(TryLockError::Error(err)) if err.kind() == io::ErrorKind::Unsupported => { + #[cfg(feature = "logging")] + warn!( + "File locks not supported on this platform. You must ensure that only a single process opens the database file, at a time" + ); + + Ok(Self { + file, + lock_supported: false, + }) + } + Err(TryLockError::Error(err)) => Err(err.into()), + } + } +} + +impl StorageBackend for FileBackend { + fn len(&self) -> Result { + Ok(self.file.metadata()?.len()) + } + + #[cfg(unix)] + fn read(&self, offset: u64, out: &mut [u8]) -> Result<(), io::Error> { + self.file.read_exact_at(out, offset)?; + Ok(()) + } + + #[cfg(target_os = "wasi")] + fn read(&self, offset: u64, out: &mut [u8]) -> Result<(), io::Error> { + read_exact_at(&self.file, out, offset)?; + Ok(()) + } + + #[cfg(windows)] + fn read(&self, mut offset: u64, out: &mut [u8]) -> Result<(), io::Error> { + let mut data_offset = 0; + while data_offset < out.len() { + let read = self.file.seek_read(&mut out[data_offset..], offset)?; + offset += read as u64; + data_offset += read; + } + Ok(()) + } + + fn set_len(&self, len: u64) -> Result<(), io::Error> { + self.file.set_len(len) + } + + fn sync_data(&self) -> Result<(), io::Error> { + self.file.sync_data() + } + + #[cfg(unix)] + fn write(&self, offset: u64, data: &[u8]) -> Result<(), io::Error> { + self.file.write_all_at(data, offset) + } + + #[cfg(target_os = "wasi")] + fn write(&self, offset: u64, data: &[u8]) -> Result<(), io::Error> { + write_all_at(&self.file, data, offset) + } + + #[cfg(windows)] + fn write(&self, mut offset: u64, data: &[u8]) -> Result<(), io::Error> { + let mut data_offset = 0; + while data_offset < data.len() { + let written = self.file.seek_write(&data[data_offset..], offset)?; + offset += written as u64; + data_offset += written; + } + Ok(()) + } + + fn close(&self) -> Result<(), io::Error> { + if self.lock_supported { + self.file.unlock()?; + } + + Ok(()) + } +} + +// TODO: replace these with wasi::FileExt when https://github.com/rust-lang/rust/issues/71213 +// is stable +#[cfg(target_os = "wasi")] +fn read_exact_at(file: &File, mut buf: &mut [u8], mut offset: u64) -> io::Result<()> { + use std::os::fd::AsRawFd; + + while !buf.is_empty() { + let nbytes = unsafe { + libc::pread( + file.as_raw_fd(), + buf.as_mut_ptr() as _, + std::cmp::min(buf.len(), libc::ssize_t::MAX as _), + offset as _, + ) + }; + match nbytes { + 0 => break, + -1 => match io::Error::last_os_error() { + err if err.kind() == io::ErrorKind::Interrupted => {} + err => return Err(err), + }, + n => { + let tmp = buf; + buf = &mut tmp[n as usize..]; + offset += n as u64; + } + } + } + if !buf.is_empty() { + Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "failed to fill whole buffer", + )) + } else { + Ok(()) + } +} + +#[cfg(target_os = "wasi")] +fn write_all_at(file: &File, mut buf: &[u8], mut offset: u64) -> io::Result<()> { + use std::os::fd::AsRawFd; + + while !buf.is_empty() { + let nbytes = unsafe { + libc::pwrite( + file.as_raw_fd(), + buf.as_ptr() as _, + std::cmp::min(buf.len(), libc::ssize_t::MAX as _), + offset as _, + ) + }; + match nbytes { + 0 => { + return Err(io::Error::new( + io::ErrorKind::WriteZero, + "failed to write whole buffer", + )); + } + -1 => match io::Error::last_os_error() { + err if err.kind() == io::ErrorKind::Interrupted => {} + err => return Err(err), + }, + n => { + buf = &buf[n as usize..]; + offset += n as u64 + } + } + } + Ok(()) +} diff --git a/v2/redb/src/tree_store/page_store/header.rs b/v2/redb/src/tree_store/page_store/header.rs new file mode 100644 index 000000000..521776411 --- /dev/null +++ b/v2/redb/src/tree_store/page_store/header.rs @@ -0,0 +1,712 @@ +use crate::transaction_tracker::TransactionId; +use crate::tree_store::Checksum; +use crate::tree_store::btree_base::BtreeHeader; +use crate::tree_store::page_store::layout::{DatabaseLayout, RegionLayout}; +use crate::tree_store::page_store::page_manager::{ + FILE_FORMAT_VERSION1, FILE_FORMAT_VERSION2, FILE_FORMAT_VERSION3, xxh3_checksum, +}; +use crate::{DatabaseError, Result, StorageError}; +use std::mem::size_of; + +// Database layout: +// +// Super-header (header + commit slots) +// The super-header length is rounded up to the nearest full page size +// +// Header (first 64 bytes): +// 9 bytes: magic number +// 1 byte: god byte +// 2 byte: padding +// 4 bytes: page size +// Definition of region +// 4 bytes: region header pages +// 4 bytes: region max data pages +// +// Commit slot 0 (next 128 bytes): +// 1 byte: version +// 1 byte: != 0 if root page is non-null +// 1 byte: != 0 if freed table root page is non-null +// 5 bytes: padding +// 8 bytes: root page +// 16 bytes: root checksum +// 8 bytes: unused: formerly freed table root page +// 16 bytes: unused: formerly freed table root checksum +// 8 bytes: last committed transaction id +// 4 bytes: number of full regions +// 4 bytes: data pages in partial trailing region +// 8 bytes: unused: formerly region tracker page number +// 16 bytes: slot checksum +// +// Commit slot 1 (next 128 bytes): +// Same layout as slot 0 + +// Inspired by PNG's magic number +pub(super) const MAGICNUMBER: [u8; 9] = [b'r', b'e', b'd', b'b', 0x1A, 0x0A, 0xA9, 0x0D, 0x0A]; +const GOD_BYTE_OFFSET: usize = MAGICNUMBER.len(); +const PAGE_SIZE_OFFSET: usize = GOD_BYTE_OFFSET + size_of::() + 2; // +2 for padding +const REGION_HEADER_PAGES_OFFSET: usize = PAGE_SIZE_OFFSET + size_of::(); +const REGION_MAX_DATA_PAGES_OFFSET: usize = REGION_HEADER_PAGES_OFFSET + size_of::(); +const NUM_FULL_REGIONS_OFFSET: usize = REGION_MAX_DATA_PAGES_OFFSET + size_of::(); +const TRAILING_REGION_DATA_PAGES_OFFSET: usize = NUM_FULL_REGIONS_OFFSET + size_of::(); +// Formerly the region tracker page +const _UNUSED3_OFFSET: usize = TRAILING_REGION_DATA_PAGES_OFFSET + size_of::(); +const TRANSACTION_SIZE: usize = 128; +const TRANSACTION_0_OFFSET: usize = 64; +const TRANSACTION_1_OFFSET: usize = TRANSACTION_0_OFFSET + TRANSACTION_SIZE; +pub(super) const DB_HEADER_SIZE: usize = TRANSACTION_1_OFFSET + TRANSACTION_SIZE; + +// God byte flags +const PRIMARY_BIT: u8 = 1; +const RECOVERY_REQUIRED: u8 = 2; +const TWO_PHASE_COMMIT: u8 = 4; + +// Structure of each commit slot +const VERSION_OFFSET: usize = 0; +const USER_ROOT_NON_NULL_OFFSET: usize = size_of::(); +const SYSTEM_ROOT_NON_NULL_OFFSET: usize = USER_ROOT_NON_NULL_OFFSET + size_of::(); +const _UNUSED_OFFSET: usize = SYSTEM_ROOT_NON_NULL_OFFSET + size_of::(); +const PADDING: usize = 4; + +const USER_ROOT_OFFSET: usize = _UNUSED_OFFSET + size_of::() + PADDING; +const SYSTEM_ROOT_OFFSET: usize = USER_ROOT_OFFSET + BtreeHeader::serialized_size(); +const _UNUSED2_OFFSET: usize = SYSTEM_ROOT_OFFSET + BtreeHeader::serialized_size(); +const TRANSACTION_ID_OFFSET: usize = _UNUSED2_OFFSET + BtreeHeader::serialized_size(); +const TRANSACTION_LAST_FIELD: usize = TRANSACTION_ID_OFFSET + size_of::(); + +const SLOT_CHECKSUM_OFFSET: usize = TRANSACTION_SIZE - size_of::(); + +pub(crate) const PAGE_SIZE: usize = 4096; + +fn get_u32(data: &[u8]) -> u32 { + u32::from_le_bytes(data[..size_of::()].try_into().unwrap()) +} + +fn get_u64(data: &[u8]) -> u64 { + u64::from_le_bytes(data[..size_of::()].try_into().unwrap()) +} + +#[derive(Copy, Clone)] +pub(super) struct HeaderRepairInfo { + pub(super) invalid_magic_number: bool, + pub(super) primary_corrupted: bool, + pub(super) secondary_corrupted: bool, +} + +#[derive(Clone)] +pub(super) struct DatabaseHeader { + primary_slot: usize, + pub(super) recovery_required: bool, + pub(super) two_phase_commit: bool, + page_size: u32, + region_header_pages: u32, + region_max_data_pages: u32, + full_regions: u32, + trailing_partial_region_pages: u32, + transaction_slots: [TransactionHeader; 2], +} + +impl DatabaseHeader { + pub(super) fn new(layout: DatabaseLayout, transaction_id: TransactionId) -> Self { + #[allow(clippy::assertions_on_constants)] + { + assert!(TRANSACTION_LAST_FIELD <= SLOT_CHECKSUM_OFFSET); + } + + let slot = TransactionHeader::new(transaction_id); + Self { + primary_slot: 0, + recovery_required: true, + two_phase_commit: false, + page_size: layout.full_region_layout().page_size(), + region_header_pages: layout.full_region_layout().get_header_pages(), + region_max_data_pages: layout.full_region_layout().num_pages(), + full_regions: layout.num_full_regions(), + trailing_partial_region_pages: layout + .trailing_region_layout() + .map(|x| x.num_pages()) + .unwrap_or_default(), + transaction_slots: [slot.clone(), slot], + } + } + + pub(super) fn page_size(&self) -> u32 { + self.page_size + } + + pub(super) fn layout(&self) -> DatabaseLayout { + let full_layout = RegionLayout::new( + self.region_max_data_pages, + self.region_header_pages, + self.page_size, + ); + let trailing = if self.trailing_partial_region_pages > 0 { + Some(RegionLayout::new( + self.trailing_partial_region_pages, + self.region_header_pages, + self.page_size, + )) + } else { + None + }; + DatabaseLayout::new(self.full_regions, full_layout, trailing) + } + + pub(super) fn set_layout(&mut self, layout: DatabaseLayout) { + assert_eq!( + self.layout().full_region_layout(), + layout.full_region_layout() + ); + if let Some(trailing) = layout.trailing_region_layout() { + assert_eq!(trailing.get_header_pages(), self.region_header_pages); + assert_eq!(trailing.page_size(), self.page_size); + self.trailing_partial_region_pages = trailing.num_pages(); + } else { + self.trailing_partial_region_pages = 0; + } + self.full_regions = layout.num_full_regions(); + } + + pub(super) fn primary_slot(&self) -> &TransactionHeader { + &self.transaction_slots[self.primary_slot] + } + + pub(super) fn secondary_slot(&self) -> &TransactionHeader { + &self.transaction_slots[self.primary_slot ^ 1] + } + + pub(super) fn secondary_slot_mut(&mut self) -> &mut TransactionHeader { + &mut self.transaction_slots[self.primary_slot ^ 1] + } + + pub(super) fn swap_primary_slot(&mut self) { + self.primary_slot ^= 1; + } + + // Figure out which slot to use as the primary when starting a repair. The repair process might + // still switch to the other slot later, if the tree checksums turn out to be invalid. + // + // Returns true if we picked the original primary, or false if we swapped + pub(super) fn pick_primary_for_repair( + &mut self, + repair_info: HeaderRepairInfo, + ) -> Result { + // If the primary was written using 2-phase commit, it's guaranteed to be valid. Don't look + // at the secondary; even if it happens to have a valid checksum, Durability::Paranoid means + // we can't trust it + if self.two_phase_commit { + if repair_info.primary_corrupted { + return Err(StorageError::Corrupted( + "Primary is corrupted despite 2-phase commit".to_string(), + )); + } + return Ok(true); + } + + // Pick whichever slot is newer, assuming it has a valid checksum. This handles an edge case + // where we crash during fsync(), and the only data that got written to disk was the god byte + // update swapping the primary -- in that case, the primary contains a valid but out-of-date + // transaction, so we need to load from the secondary instead + if repair_info.primary_corrupted { + if repair_info.secondary_corrupted { + return Err(StorageError::Corrupted( + "Both commit slots are corrupted".to_string(), + )); + } + self.swap_primary_slot(); + return Ok(false); + } + + let secondary_newer = + self.secondary_slot().transaction_id > self.primary_slot().transaction_id; + if secondary_newer && !repair_info.secondary_corrupted { + self.swap_primary_slot(); + return Ok(false); + } + + Ok(true) + } + + // TODO: consider returning an Err with the repair info + pub(super) fn from_bytes(data: &[u8]) -> Result<(Self, HeaderRepairInfo), DatabaseError> { + let invalid_magic_number = data[..MAGICNUMBER.len()] != MAGICNUMBER; + + let primary_slot = usize::from(data[GOD_BYTE_OFFSET] & PRIMARY_BIT != 0); + let recovery_required = (data[GOD_BYTE_OFFSET] & RECOVERY_REQUIRED) != 0; + let two_phase_commit = (data[GOD_BYTE_OFFSET] & TWO_PHASE_COMMIT) != 0; + let page_size = get_u32(&data[PAGE_SIZE_OFFSET..]); + let region_header_pages = get_u32(&data[REGION_HEADER_PAGES_OFFSET..]); + let region_max_data_pages = get_u32(&data[REGION_MAX_DATA_PAGES_OFFSET..]); + let full_regions = get_u32(&data[NUM_FULL_REGIONS_OFFSET..]); + let trailing_data_pages = get_u32(&data[TRAILING_REGION_DATA_PAGES_OFFSET..]); + let (slot0, slot0_corrupted) = TransactionHeader::from_bytes( + &data[TRANSACTION_0_OFFSET..(TRANSACTION_0_OFFSET + TRANSACTION_SIZE)], + )?; + let (slot1, slot1_corrupted) = TransactionHeader::from_bytes( + &data[TRANSACTION_1_OFFSET..(TRANSACTION_1_OFFSET + TRANSACTION_SIZE)], + )?; + let (primary_corrupted, secondary_corrupted) = if primary_slot == 0 { + (slot0_corrupted, slot1_corrupted) + } else { + (slot1_corrupted, slot0_corrupted) + }; + + let result = Self { + primary_slot, + recovery_required, + two_phase_commit, + page_size, + region_header_pages, + region_max_data_pages, + full_regions, + trailing_partial_region_pages: trailing_data_pages, + transaction_slots: [slot0, slot1], + }; + let repair = HeaderRepairInfo { + invalid_magic_number, + primary_corrupted, + secondary_corrupted, + }; + Ok((result, repair)) + } + + pub(super) fn to_bytes(&self, include_magic_number: bool) -> [u8; DB_HEADER_SIZE] { + let mut result = [0; DB_HEADER_SIZE]; + if include_magic_number { + result[..MAGICNUMBER.len()].copy_from_slice(&MAGICNUMBER); + } + result[GOD_BYTE_OFFSET] = self.primary_slot.try_into().unwrap(); + if self.recovery_required { + result[GOD_BYTE_OFFSET] |= RECOVERY_REQUIRED; + } + if self.two_phase_commit { + result[GOD_BYTE_OFFSET] |= TWO_PHASE_COMMIT; + } + result[PAGE_SIZE_OFFSET..(PAGE_SIZE_OFFSET + size_of::())] + .copy_from_slice(&self.page_size.to_le_bytes()); + result[REGION_HEADER_PAGES_OFFSET..(REGION_HEADER_PAGES_OFFSET + size_of::())] + .copy_from_slice(&self.region_header_pages.to_le_bytes()); + result[REGION_MAX_DATA_PAGES_OFFSET..(REGION_MAX_DATA_PAGES_OFFSET + size_of::())] + .copy_from_slice(&self.region_max_data_pages.to_le_bytes()); + result[NUM_FULL_REGIONS_OFFSET..(NUM_FULL_REGIONS_OFFSET + size_of::())] + .copy_from_slice(&self.full_regions.to_le_bytes()); + result[TRAILING_REGION_DATA_PAGES_OFFSET + ..(TRAILING_REGION_DATA_PAGES_OFFSET + size_of::())] + .copy_from_slice(&self.trailing_partial_region_pages.to_le_bytes()); + let slot0 = self.transaction_slots[0].to_bytes(); + result[TRANSACTION_0_OFFSET..(TRANSACTION_0_OFFSET + slot0.len())].copy_from_slice(&slot0); + let slot1 = self.transaction_slots[1].to_bytes(); + result[TRANSACTION_1_OFFSET..(TRANSACTION_1_OFFSET + slot1.len())].copy_from_slice(&slot1); + + result + } +} + +#[derive(Clone)] +pub(super) struct TransactionHeader { + pub(super) version: u8, + pub(super) user_root: Option, + pub(super) system_root: Option, + pub(super) transaction_id: TransactionId, +} + +impl TransactionHeader { + fn new(transaction_id: TransactionId) -> Self { + Self { + version: FILE_FORMAT_VERSION3, + user_root: None, + system_root: None, + transaction_id, + } + } + + // Returned bool indicates whether the checksum was corrupted + pub(super) fn from_bytes(data: &[u8]) -> Result<(Self, bool), DatabaseError> { + let version = data[VERSION_OFFSET]; + match version { + FILE_FORMAT_VERSION1 | FILE_FORMAT_VERSION2 => { + return Err(DatabaseError::UpgradeRequired(version)); + } + FILE_FORMAT_VERSION3 => {} + _ => { + return Err(StorageError::Corrupted(format!( + "Expected file format version <= {FILE_FORMAT_VERSION3}, found {version}", + )) + .into()); + } + } + let checksum = Checksum::from_le_bytes( + data[SLOT_CHECKSUM_OFFSET..(SLOT_CHECKSUM_OFFSET + size_of::())] + .try_into() + .unwrap(), + ); + let corrupted = checksum != xxh3_checksum(&data[..SLOT_CHECKSUM_OFFSET]); + + let user_root = if data[USER_ROOT_NON_NULL_OFFSET] != 0 { + Some(BtreeHeader::from_le_bytes( + data[USER_ROOT_OFFSET..(USER_ROOT_OFFSET + BtreeHeader::serialized_size())] + .try_into() + .unwrap(), + )) + } else { + None + }; + let system_root = if data[SYSTEM_ROOT_NON_NULL_OFFSET] != 0 { + Some(BtreeHeader::from_le_bytes( + data[SYSTEM_ROOT_OFFSET..(SYSTEM_ROOT_OFFSET + BtreeHeader::serialized_size())] + .try_into() + .unwrap(), + )) + } else { + None + }; + let transaction_id = TransactionId::new(get_u64(&data[TRANSACTION_ID_OFFSET..])); + + let result = Self { + version, + user_root, + system_root, + transaction_id, + }; + + Ok((result, corrupted)) + } + + pub(super) fn to_bytes(&self) -> [u8; TRANSACTION_SIZE] { + assert_eq!(self.version, FILE_FORMAT_VERSION3); + let mut result = [0; TRANSACTION_SIZE]; + result[VERSION_OFFSET] = self.version; + if let Some(header) = self.user_root { + result[USER_ROOT_NON_NULL_OFFSET] = 1; + result[USER_ROOT_OFFSET..(USER_ROOT_OFFSET + BtreeHeader::serialized_size())] + .copy_from_slice(&header.to_le_bytes()); + } + if let Some(header) = self.system_root { + result[SYSTEM_ROOT_NON_NULL_OFFSET] = 1; + result[SYSTEM_ROOT_OFFSET..(SYSTEM_ROOT_OFFSET + BtreeHeader::serialized_size())] + .copy_from_slice(&header.to_le_bytes()); + } + result[TRANSACTION_ID_OFFSET..(TRANSACTION_ID_OFFSET + size_of::())] + .copy_from_slice(&self.transaction_id.raw_id().to_le_bytes()); + let checksum = xxh3_checksum(&result[..SLOT_CHECKSUM_OFFSET]); + result[SLOT_CHECKSUM_OFFSET..(SLOT_CHECKSUM_OFFSET + size_of::())] + .copy_from_slice(&checksum.to_le_bytes()); + + result + } +} + +#[cfg(test)] +mod test { + use crate::backends::FileBackend; + use crate::db::TableDefinition; + use crate::tree_store::page_store::header::{ + GOD_BYTE_OFFSET, MAGICNUMBER, PRIMARY_BIT, RECOVERY_REQUIRED, TRANSACTION_0_OFFSET, + TRANSACTION_1_OFFSET, TWO_PHASE_COMMIT, USER_ROOT_OFFSET, + }; + use crate::{Database, DatabaseError, ReadableTable, StorageBackend}; + use crate::{ReadableDatabase, StorageError}; + use std::fs::OpenOptions; + use std::io::{Error, ErrorKind, Read, Seek, SeekFrom, Write}; + use std::mem::size_of; + use std::sync::Arc; + use std::sync::atomic::{AtomicBool, Ordering}; + + const X: TableDefinition<&str, &str> = TableDefinition::new("x"); + + #[derive(Debug)] + struct FailingBackend { + inner: FileBackend, + fail: Arc, + } + + impl FailingBackend { + fn new(backend: FileBackend) -> Self { + Self { + inner: backend, + fail: Arc::new(AtomicBool::new(false)), + } + } + + fn check_fail(&self) -> Result<(), std::io::Error> { + if self.fail.load(Ordering::SeqCst) { + return Err(std::io::Error::from(ErrorKind::Other)); + } + + Ok(()) + } + } + + impl StorageBackend for FailingBackend { + fn len(&self) -> Result { + self.check_fail()?; + self.inner.len() + } + + fn read(&self, offset: u64, out: &mut [u8]) -> Result<(), std::io::Error> { + self.check_fail()?; + self.inner.read(offset, out) + } + + fn set_len(&self, len: u64) -> Result<(), std::io::Error> { + self.check_fail()?; + self.inner.set_len(len) + } + + fn sync_data(&self) -> Result<(), std::io::Error> { + self.check_fail()?; + self.inner.sync_data() + } + + fn write(&self, offset: u64, data: &[u8]) -> Result<(), std::io::Error> { + self.check_fail()?; + self.inner.write(offset, data) + } + + fn close(&self) -> Result<(), Error> { + self.inner.close() + } + } + + #[test] + fn repair_allocator_checksums() { + let tmpfile = crate::create_tempfile(); + let cloned = OpenOptions::new() + .read(true) + .write(true) + .open(tmpfile.path()) + .unwrap(); + let backend = FailingBackend::new(FileBackend::new(cloned).unwrap()); + let fail = backend.fail.clone(); + let db = Database::builder().create_with_backend(backend).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(X).unwrap(); + table.insert("hello", "world").unwrap(); + } + write_txn.commit().unwrap(); + + // Start a read to be sure the previous write isn't garbage collected + let read_txn = db.begin_read().unwrap(); + + let mut write_txn = db.begin_write().unwrap(); + { + write_txn.set_quick_repair(true); + let mut table = write_txn.open_table(X).unwrap(); + table.insert("hello", "world2").unwrap(); + } + write_txn.commit().unwrap(); + drop(read_txn); + // We want our commit to be the last commit in the database, so block the Database drop() + // method from performing its own commit to trim the file + fail.store(true, Ordering::SeqCst); + drop(db); + + let mut file = tmpfile.as_file(); + + file.seek(SeekFrom::Start(GOD_BYTE_OFFSET as u64)).unwrap(); + let mut buffer = [0u8; 1]; + file.read_exact(&mut buffer).unwrap(); + file.seek(SeekFrom::Start(GOD_BYTE_OFFSET as u64)).unwrap(); + buffer[0] |= RECOVERY_REQUIRED; + buffer[0] &= !TWO_PHASE_COMMIT; + file.write_all(&buffer).unwrap(); + + // Overwrite the primary checksum to simulate a failure during commit + let primary_slot_offset = if buffer[0] & PRIMARY_BIT == 0 { + TRANSACTION_0_OFFSET + } else { + TRANSACTION_1_OFFSET + }; + file.seek(SeekFrom::Start( + (primary_slot_offset + USER_ROOT_OFFSET) as u64, + )) + .unwrap(); + file.write_all(&[0; size_of::()]).unwrap(); + + #[allow(unused_mut)] + let mut db2 = Database::create(tmpfile.path()).unwrap(); + let write_txn = db2.begin_write().unwrap(); + { + let mut table = write_txn.open_table(X).unwrap(); + assert_eq!(table.get("hello").unwrap().unwrap().value(), "world"); + table.insert("hello2", "world2").unwrap(); + } + write_txn.commit().unwrap(); + + // Locks are exclusive on Windows, so we can't concurrently overwrite the file + #[cfg(not(target_os = "windows"))] + { + file.seek(SeekFrom::Start(GOD_BYTE_OFFSET as u64)).unwrap(); + let mut buffer = [0u8; 1]; + file.read_exact(&mut buffer).unwrap(); + + // Overwrite the primary checksum to simulate a failure during commit + let primary_slot_offset = if buffer[0] & PRIMARY_BIT == 0 { + TRANSACTION_0_OFFSET + } else { + TRANSACTION_1_OFFSET + }; + file.seek(SeekFrom::Start( + (primary_slot_offset + USER_ROOT_OFFSET) as u64, + )) + .unwrap(); + file.write_all(&[0; size_of::()]).unwrap(); + + assert!(!db2.check_integrity().unwrap()); + + // Overwrite both checksums to simulate corruption + file.seek(SeekFrom::Start(GOD_BYTE_OFFSET as u64)).unwrap(); + let mut buffer = [0u8; 1]; + file.read_exact(&mut buffer).unwrap(); + + file.seek(SeekFrom::Start( + (TRANSACTION_0_OFFSET + USER_ROOT_OFFSET) as u64, + )) + .unwrap(); + file.write_all(&[0; size_of::()]).unwrap(); + file.seek(SeekFrom::Start( + (TRANSACTION_1_OFFSET + USER_ROOT_OFFSET) as u64, + )) + .unwrap(); + file.write_all(&[0; size_of::()]).unwrap(); + + assert!(matches!( + db2.check_integrity().unwrap_err(), + DatabaseError::Storage(StorageError::Corrupted(_)) + )); + } + } + + #[test] + fn repair_empty() { + let tmpfile = crate::create_tempfile(); + let db = Database::builder().create(tmpfile.path()).unwrap(); + drop(db); + + let mut file = tmpfile.as_file(); + + file.seek(SeekFrom::Start(GOD_BYTE_OFFSET as u64)).unwrap(); + let mut buffer = [0u8; 1]; + file.read_exact(&mut buffer).unwrap(); + file.seek(SeekFrom::Start(GOD_BYTE_OFFSET as u64)).unwrap(); + buffer[0] |= RECOVERY_REQUIRED; + file.write_all(&buffer).unwrap(); + + Database::open(tmpfile.path()).unwrap(); + } + + #[test] + fn close_on_drop() { + let tmpfile = crate::create_tempfile(); + let db = Database::builder() + .set_cache_size(0) + .create(tmpfile.path()) + .unwrap(); + let table_def: TableDefinition = TableDefinition::new("x"); + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(table_def).unwrap(); + table.insert(0, 0).unwrap(); + } + txn.commit().unwrap(); + let txn = db.begin_read().unwrap(); + drop(db); + assert!(matches!( + txn.list_tables().err().unwrap(), + StorageError::DatabaseClosed + )); + + let mut file = tmpfile.as_file(); + + file.seek(SeekFrom::Start(GOD_BYTE_OFFSET as u64)).unwrap(); + let mut buffer = [0u8; 1]; + file.read_exact(&mut buffer).unwrap(); + assert_eq!(buffer[0] & RECOVERY_REQUIRED, 0); + drop(txn); + } + + #[test] + fn abort_repair() { + let tmpfile = crate::create_tempfile(); + let db = Database::builder().create(tmpfile.path()).unwrap(); + drop(db); + + let mut file = tmpfile.as_file(); + + file.seek(SeekFrom::Start(GOD_BYTE_OFFSET as u64)).unwrap(); + let mut buffer = [0u8; 1]; + file.read_exact(&mut buffer).unwrap(); + file.seek(SeekFrom::Start(GOD_BYTE_OFFSET as u64)).unwrap(); + buffer[0] |= RECOVERY_REQUIRED; + buffer[0] &= !TWO_PHASE_COMMIT; + file.write_all(&buffer).unwrap(); + + let err = Database::builder() + .set_repair_callback(|handle| handle.abort()) + .open(tmpfile.path()) + .unwrap_err(); + assert!(matches!(err, DatabaseError::RepairAborted)); + } + + #[test] + fn repair_insert_reserve_regression() { + let tmpfile = crate::create_tempfile(); + let db = Database::builder().create(tmpfile.path()).unwrap(); + + let def: TableDefinition<&str, &[u8]> = TableDefinition::new("x"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(def).unwrap(); + let mut value = table.insert_reserve("hello", 5).unwrap(); + value.as_mut().copy_from_slice(b"world"); + } + write_txn.commit().unwrap(); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(def).unwrap(); + let mut value = table.insert_reserve("hello2", 5).unwrap(); + value.as_mut().copy_from_slice(b"world"); + } + write_txn.commit().unwrap(); + + drop(db); + + let mut file = tmpfile.as_file(); + + file.seek(SeekFrom::Start(GOD_BYTE_OFFSET as u64)).unwrap(); + let mut buffer = [0u8; 1]; + file.read_exact(&mut buffer).unwrap(); + file.seek(SeekFrom::Start(GOD_BYTE_OFFSET as u64)).unwrap(); + buffer[0] |= RECOVERY_REQUIRED; + file.write_all(&buffer).unwrap(); + + Database::open(tmpfile.path()).unwrap(); + } + + #[test] + fn magic_number() { + // Test compliance with some, but not all, provisions recommended by + // IETF Memo "Care and Feeding of Magic Numbers" + + // Test that magic number is not valid utf-8 + #[allow(invalid_from_utf8)] + { + assert!(std::str::from_utf8(&MAGICNUMBER).is_err()); + } + // Test there is a octet with high-bit set + assert!(MAGICNUMBER.iter().any(|x| *x & 0x80 != 0)); + // Test there is a non-printable ASCII character + assert!(MAGICNUMBER.iter().any(|x| *x < 0x20 || *x > 0x7E)); + // Test there is a printable ASCII character + assert!(MAGICNUMBER.iter().any(|x| *x >= 0x20 && *x <= 0x7E)); + // Test there is a printable ISO-8859 that's non-ASCII printable + assert!(MAGICNUMBER.iter().any(|x| *x >= 0xA0)); + // Test there is a ISO-8859 control character other than 0x09, 0x0A, 0x0C, 0x0D + assert!(MAGICNUMBER.iter().any(|x| *x < 0x09 + || *x == 0x0B + || (0x0E <= *x && *x <= 0x1F) + || (0x7F <= *x && *x <= 0x9F))); + } +} diff --git a/v2/redb/src/tree_store/page_store/layout.rs b/v2/redb/src/tree_store/page_store/layout.rs new file mode 100644 index 000000000..a42352afd --- /dev/null +++ b/v2/redb/src/tree_store/page_store/layout.rs @@ -0,0 +1,250 @@ +use std::ops::Range; + +fn round_up_to_multiple_of(value: u64, multiple: u64) -> u64 { + if value % multiple == 0 { + value + } else { + value + multiple - value % multiple + } +} + +// Regions are laid out starting with the allocator state header, followed by the pages aligned +// to the next page +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) struct RegionLayout { + num_pages: u32, + // Offset where data pages start + header_pages: u32, + page_size: u32, +} + +impl RegionLayout { + pub(super) fn new(num_pages: u32, header_pages: u32, page_size: u32) -> Self { + assert!(num_pages > 0); + Self { + num_pages, + header_pages, + page_size, + } + } + + pub(super) fn calculate( + desired_usable_bytes: u64, + page_capacity: u32, + region_header_pages: u32, + page_size: u32, + ) -> RegionLayout { + assert!(desired_usable_bytes <= u64::from(page_capacity) * u64::from(page_size)); + let num_pages = + round_up_to_multiple_of(desired_usable_bytes, page_size.into()) / u64::from(page_size); + + Self { + num_pages: num_pages.try_into().unwrap(), + header_pages: region_header_pages, + page_size, + } + } + + pub(super) fn data_section(&self) -> Range { + let header_bytes = u64::from(self.header_pages) * u64::from(self.page_size); + header_bytes..(header_bytes + self.usable_bytes()) + } + + pub(super) fn get_header_pages(&self) -> u32 { + self.header_pages + } + + pub(super) fn num_pages(&self) -> u32 { + self.num_pages + } + + pub(super) fn page_size(&self) -> u32 { + self.page_size + } + + pub(super) fn len(&self) -> u64 { + u64::from(self.header_pages) * u64::from(self.page_size) + self.usable_bytes() + } + + pub(super) fn usable_bytes(&self) -> u64 { + u64::from(self.page_size) * u64::from(self.num_pages) + } +} + +#[derive(Clone, Copy, Debug)] +pub(crate) struct DatabaseLayout { + full_region_layout: RegionLayout, + num_full_regions: u32, + trailing_partial_region: Option, +} + +impl DatabaseLayout { + pub(super) fn new( + full_regions: u32, + full_region: RegionLayout, + trailing_region: Option, + ) -> Self { + Self { + full_region_layout: full_region, + num_full_regions: full_regions, + trailing_partial_region: trailing_region, + } + } + + pub(super) fn reduce_last_region(&mut self, pages: u32) { + if let Some(ref mut trailing) = self.trailing_partial_region { + assert!(pages <= trailing.num_pages); + trailing.num_pages -= pages; + if trailing.num_pages == 0 { + self.trailing_partial_region = None; + } + } else { + self.num_full_regions -= 1; + let full_layout = self.full_region_layout; + if full_layout.num_pages > pages { + self.trailing_partial_region = Some(RegionLayout::new( + full_layout.num_pages - pages, + full_layout.header_pages, + full_layout.page_size, + )); + } + } + } + + pub(super) fn recalculate( + file_len: u64, + region_header_pages_u32: u32, + region_max_data_pages_u32: u32, + page_size_u32: u32, + ) -> Self { + let page_size = u64::from(page_size_u32); + let region_header_pages = u64::from(region_header_pages_u32); + let region_max_data_pages = u64::from(region_max_data_pages_u32); + // Super-header + let mut remaining = file_len - page_size; + let full_region_size = (region_header_pages + region_max_data_pages) * page_size; + let full_regions = remaining / full_region_size; + remaining -= full_regions * full_region_size; + let trailing = if remaining >= (region_header_pages + 1) * page_size { + remaining -= region_header_pages * page_size; + let remaining: u32 = remaining.try_into().unwrap(); + let data_pages = remaining / page_size_u32; + assert!(data_pages < region_max_data_pages_u32); + Some(RegionLayout::new( + data_pages, + region_header_pages_u32, + page_size_u32, + )) + } else { + None + }; + let full_layout = RegionLayout::new( + region_max_data_pages_u32, + region_header_pages_u32, + page_size_u32, + ); + + Self { + full_region_layout: full_layout, + num_full_regions: full_regions.try_into().unwrap(), + trailing_partial_region: trailing, + } + } + + pub(super) fn calculate( + desired_usable_bytes: u64, + page_capacity: u32, + region_header_pages: u32, + page_size: u32, + ) -> Self { + let full_region_layout = RegionLayout::new(page_capacity, region_header_pages, page_size); + if desired_usable_bytes <= full_region_layout.usable_bytes() { + // Single region layout + let region_layout = RegionLayout::calculate( + desired_usable_bytes, + page_capacity, + region_header_pages, + page_size, + ); + DatabaseLayout { + full_region_layout, + num_full_regions: 0, + trailing_partial_region: Some(region_layout), + } + } else { + // Multi region layout + let full_regions = desired_usable_bytes / full_region_layout.usable_bytes(); + let remaining_desired = + desired_usable_bytes - full_regions * full_region_layout.usable_bytes(); + assert!(full_regions > 0); + let trailing_region = if remaining_desired > 0 { + Some(RegionLayout::calculate( + remaining_desired, + page_capacity, + region_header_pages, + page_size, + )) + } else { + None + }; + if let Some(ref region) = trailing_region { + // All regions must have the same header size + assert_eq!(region.header_pages, full_region_layout.header_pages); + } + DatabaseLayout { + full_region_layout, + num_full_regions: full_regions.try_into().unwrap(), + trailing_partial_region: trailing_region, + } + } + } + + pub(super) fn full_region_layout(&self) -> &RegionLayout { + &self.full_region_layout + } + + pub(super) fn trailing_region_layout(&self) -> Option<&RegionLayout> { + self.trailing_partial_region.as_ref() + } + + pub(super) fn num_full_regions(&self) -> u32 { + self.num_full_regions + } + + pub(super) fn num_regions(&self) -> u32 { + if self.trailing_partial_region.is_some() { + self.num_full_regions + 1 + } else { + self.num_full_regions + } + } + + pub(super) fn len(&self) -> u64 { + let last = self.num_regions() - 1; + self.region_base_address(last) + self.region_layout(last).len() + } + + pub(super) fn usable_bytes(&self) -> u64 { + let trailing = self + .trailing_partial_region + .as_ref() + .map(RegionLayout::usable_bytes) + .unwrap_or_default(); + u64::from(self.num_full_regions) * self.full_region_layout.usable_bytes() + trailing + } + + pub(super) fn region_base_address(&self, region: u32) -> u64 { + assert!(region < self.num_regions()); + u64::from(self.full_region_layout.page_size()) + + u64::from(region) * self.full_region_layout.len() + } + + pub(super) fn region_layout(&self, region: u32) -> RegionLayout { + assert!(region < self.num_regions()); + if region == self.num_full_regions { + self.trailing_partial_region.unwrap() + } else { + self.full_region_layout + } + } +} diff --git a/v2/redb/src/tree_store/page_store/lru_cache.rs b/v2/redb/src/tree_store/page_store/lru_cache.rs new file mode 100644 index 000000000..8b15d4d00 --- /dev/null +++ b/v2/redb/src/tree_store/page_store/lru_cache.rs @@ -0,0 +1,103 @@ +use crate::tree_store::page_store::fast_hash::FastHashMapU64; +use std::collections::VecDeque; +use std::sync::atomic::{AtomicBool, Ordering}; + +#[derive(Default)] +pub struct LRUCache { + // AtomicBool is the second chance flag + cache: FastHashMapU64<(T, AtomicBool)>, + lru_queue: VecDeque, +} + +impl LRUCache { + pub(crate) fn new() -> Self { + Self { + cache: Default::default(), + lru_queue: Default::default(), + } + } + + pub(crate) fn len(&self) -> usize { + self.cache.len() + } + + pub(crate) fn insert(&mut self, key: u64, value: T) -> Option { + let result = self + .cache + .insert(key, (value, AtomicBool::new(false))) + .map(|(x, _)| x); + if result.is_none() { + self.lru_queue.push_back(key); + } + result + } + + pub(crate) fn remove(&mut self, key: u64) -> Option { + if let Some((value, _)) = self.cache.remove(&key) { + if self.lru_queue.len() > 2 * self.cache.len() { + // Cycle two elements of the LRU queue to ensure it doesn't grow without bound + for _ in 0..2 { + if let Some(removed_key) = self.lru_queue.pop_front() + && let Some((_, second_chance)) = self.cache.get(&removed_key) + { + second_chance.store(false, Ordering::Release); + self.lru_queue.push_back(removed_key); + } + } + } + Some(value) + } else { + None + } + } + + pub(crate) fn get(&self, key: u64) -> Option<&T> { + if let Some((value, second_chance)) = self.cache.get(&key) { + second_chance.store(true, Ordering::Release); + Some(value) + } else { + None + } + } + + pub(crate) fn get_mut(&mut self, key: u64) -> Option<&mut T> { + if let Some((value, second_chance)) = self.cache.get_mut(&key) { + second_chance.store(true, Ordering::Release); + Some(value) + } else { + None + } + } + + pub(crate) fn iter(&self) -> impl ExactSizeIterator { + self.cache.iter().map(|(k, (v, _))| (k, v)) + } + + pub(crate) fn iter_mut(&mut self) -> impl ExactSizeIterator { + self.cache.iter_mut().map(|(k, (v, _))| (k, v)) + } + + pub(crate) fn pop_lowest_priority(&mut self) -> Option<(u64, T)> { + while let Some(key) = self.lru_queue.pop_front() { + if let Some((_, second_chance)) = self.cache.get(&key) { + if second_chance + .compare_exchange(true, false, Ordering::AcqRel, Ordering::Acquire) + .is_ok() + { + self.lru_queue.push_back(key); + } else { + let (value, _) = self.cache.remove(&key).unwrap(); + return Some((key, value)); + } + } + } + None + } + + pub(crate) fn clear(&mut self) { + self.cache.shrink_to_fit(); + self.cache.clear(); + self.lru_queue.shrink_to_fit(); + self.lru_queue.clear(); + } +} diff --git a/v2/redb/src/tree_store/page_store/mod.rs b/v2/redb/src/tree_store/page_store/mod.rs new file mode 100644 index 000000000..08460bcaa --- /dev/null +++ b/v2/redb/src/tree_store/page_store/mod.rs @@ -0,0 +1,30 @@ +mod backends; +mod base; +mod bitmap; +mod buddy_allocator; +mod cached_file; +mod fast_hash; +pub mod file_backend; +mod header; +mod layout; +mod lru_cache; +mod page_manager; +mod region; +mod savepoint; +#[allow(clippy::pedantic, dead_code)] +mod xxh3; + +pub use backends::InMemoryBackend; +pub(crate) use backends::ReadOnlyBackend; +pub(crate) use base::{ + MAX_PAIR_LENGTH, MAX_VALUE_LENGTH, Page, PageHint, PageNumber, PageTrackerPolicy, +}; +pub(crate) use header::PAGE_SIZE; +pub(crate) use page_manager::{ + FILE_FORMAT_VERSION3, ShrinkPolicy, TransactionalMemory, xxh3_checksum, +}; +pub use savepoint::Savepoint; +pub(crate) use savepoint::SerializedSavepoint; + +pub(super) use base::{PageImpl, PageMut}; +pub(super) use xxh3::hash128_with_seed; diff --git a/v2/redb/src/tree_store/page_store/page_manager.rs b/v2/redb/src/tree_store/page_store/page_manager.rs new file mode 100644 index 000000000..30dcf8d5a --- /dev/null +++ b/v2/redb/src/tree_store/page_store/page_manager.rs @@ -0,0 +1,1231 @@ +use crate::transaction_tracker::TransactionId; +use crate::transactions::{AllocatorStateKey, AllocatorStateTree, AllocatorStateTreeMut}; +use crate::tree_store::btree_base::{BtreeHeader, Checksum}; +use crate::tree_store::page_store::base::{MAX_PAGE_INDEX, PageHint}; +use crate::tree_store::page_store::buddy_allocator::BuddyAllocator; +use crate::tree_store::page_store::cached_file::PagedCachedFile; +use crate::tree_store::page_store::fast_hash::PageNumberHashSet; +use crate::tree_store::page_store::header::{DB_HEADER_SIZE, DatabaseHeader, MAGICNUMBER}; +use crate::tree_store::page_store::layout::DatabaseLayout; +use crate::tree_store::page_store::region::{Allocators, RegionTracker}; +use crate::tree_store::page_store::{PageImpl, PageMut, hash128_with_seed}; +use crate::tree_store::{Page, PageNumber, PageTrackerPolicy}; +use crate::{CacheStats, StorageBackend}; +use crate::{DatabaseError, Result, StorageError}; +use std::cmp::{max, min}; +#[cfg(debug_assertions)] +use std::collections::HashMap; +#[cfg(debug_assertions)] +use std::collections::HashSet; +use std::convert::TryInto; +use std::io::ErrorKind; +use std::marker::PhantomData; +#[cfg(debug_assertions)] +use std::sync::Arc; +use std::sync::Mutex; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::thread; + +// The region header is optional in the v3 file format +// It's an artifact of the v2 file format, so we initialize new databases without headers to save space +const NO_HEADER: u32 = 0; + +// Regions have a maximum size of 4GiB. A `4GiB - overhead` value is the largest that can be represented, +// because the leaf node format uses 32bit offsets +const MAX_USABLE_REGION_SPACE: u64 = 4 * 1024 * 1024 * 1024; +// TODO: remove this constant? +pub(crate) const MAX_MAX_PAGE_ORDER: u8 = 20; +pub(super) const MIN_USABLE_PAGES: u32 = 10; +const MIN_DESIRED_USABLE_BYTES: u64 = 1024 * 1024; + +pub(super) const INITIAL_REGIONS: u32 = 1000; // Enough for a 4TiB database + +// Original file format. No lengths stored with btrees +pub(crate) const FILE_FORMAT_VERSION1: u8 = 1; +// New file format. All btrees have a separate length stored in their header for constant time access +pub(crate) const FILE_FORMAT_VERSION2: u8 = 2; +// New file format: +// * Allocator state is stored in a system table, instead of in the region headers +// * Freed tree split into two system tables: one for the data tables, and one for the system tables +// It is no longer stored in a separate tree +// * New "allocated pages table" which tracks the pages allocated, in the data tree, by a transaction. +// This is a system table. It is only written when a savepoint exists +// * New persistent savepoint format +pub(crate) const FILE_FORMAT_VERSION3: u8 = 3; + +#[derive(Copy, Clone)] +pub(crate) enum ShrinkPolicy { + // Try to shrink the file by the default amount + Default, + // Try to shrink the file by the maximum amount + Maximum, + // Do not try to shrink the file + Never, +} + +fn ceil_log2(x: usize) -> u8 { + if x.is_power_of_two() { + x.trailing_zeros().try_into().unwrap() + } else { + x.next_power_of_two().trailing_zeros().try_into().unwrap() + } +} + +pub(crate) fn xxh3_checksum(data: &[u8]) -> Checksum { + hash128_with_seed(data, 0) +} + +struct InMemoryState { + header: DatabaseHeader, + // TODO: we should make this an Option because it is only valid after the Database initializes it + allocators: Allocators, +} + +impl InMemoryState { + fn new(header: DatabaseHeader) -> Self { + let allocators = Allocators::new(header.layout()); + Self { header, allocators } + } + + fn get_region(&self, region: u32) -> &BuddyAllocator { + &self.allocators.region_allocators[region as usize] + } + + fn get_region_mut(&mut self, region: u32) -> &mut BuddyAllocator { + &mut self.allocators.region_allocators[region as usize] + } + + fn get_region_tracker_mut(&mut self) -> &mut RegionTracker { + &mut self.allocators.region_tracker + } +} + +pub(crate) struct TransactionalMemory { + // Pages allocated since the last commit + // TODO: maybe this should be moved to WriteTransaction? + allocated_since_commit: Mutex, + unpersisted: Mutex, + // True if the allocator state was corrupted when the file was opened + // TODO: maybe we can remove this flag now that CheckedBackend exists? + needs_recovery: AtomicBool, + storage: PagedCachedFile, + state: Mutex, + // The number of PageMut which are outstanding + #[cfg(debug_assertions)] + open_dirty_pages: Arc>>, + // Reference counts of PageImpls that are outstanding + #[cfg(debug_assertions)] + read_page_ref_counts: Arc>>, + // Set of all allocated pages for debugging assertions + #[cfg(debug_assertions)] + allocated_pages: Arc>, + // Indicates that a non-durable commit has been made, so reads should be served from the secondary meta page + read_from_secondary: AtomicBool, + page_size: u32, + // We store these separately from the layout because they're static, and accessed on the get_page() + // code path where there is no locking + region_size: u64, + region_header_with_padding_size: u64, +} + +impl TransactionalMemory { + pub(crate) fn new( + file: Box, + // Allow initializing a new database in an empty file + allow_initialize: bool, + page_size: usize, + requested_region_size: Option, + cache_size: usize, + read_only: bool, + ) -> Result { + assert!(page_size.is_power_of_two() && page_size >= DB_HEADER_SIZE); + + let region_size = requested_region_size.unwrap_or(MAX_USABLE_REGION_SPACE); + let region_size = min( + region_size, + (u64::from(MAX_PAGE_INDEX) + 1) * page_size as u64, + ); + assert!(region_size.is_power_of_two()); + + let storage = PagedCachedFile::new(file, page_size as u64, cache_size)?; + + let initial_storage_len = storage.raw_file_len()?; + + let magic_number: [u8; MAGICNUMBER.len()] = + if initial_storage_len >= MAGICNUMBER.len() as u64 { + storage + .read_direct(0, MAGICNUMBER.len())? + .try_into() + .unwrap() + } else { + [0; MAGICNUMBER.len()] + }; + + if initial_storage_len > 0 { + // File already exists check that the magic number matches + if magic_number != MAGICNUMBER { + return Err(StorageError::Io(ErrorKind::InvalidData.into()).into()); + } + } else { + // File is empty, check that we're allowed to initialize a new database (i.e. the caller is Database::create() and not open()) + if !allow_initialize { + return Err(StorageError::Io(ErrorKind::InvalidData.into()).into()); + } + } + + if magic_number != MAGICNUMBER { + let region_tracker_required_bytes = + RegionTracker::new(INITIAL_REGIONS, MAX_MAX_PAGE_ORDER + 1) + .to_vec() + .len(); + + // Make sure that there is enough room to allocate the region tracker into a page + let size: u64 = max( + MIN_DESIRED_USABLE_BYTES, + page_size as u64 * u64::from(MIN_USABLE_PAGES), + ); + let tracker_space = + (page_size * region_tracker_required_bytes.div_ceil(page_size)) as u64; + let starting_size = size + tracker_space; + + let page_capacity = (region_size / u64::try_from(page_size).unwrap()) + .try_into() + .unwrap(); + let layout = DatabaseLayout::calculate( + starting_size, + page_capacity, + NO_HEADER, + page_size.try_into().unwrap(), + ); + + { + let file_len = storage.raw_file_len()?; + + if file_len < layout.len() { + storage.resize(layout.len())?; + } + } + + let mut header = DatabaseHeader::new(layout, TransactionId::new(0)); + + header.recovery_required = false; + header.two_phase_commit = true; + storage + .write(0, DB_HEADER_SIZE, true)? + .mem_mut() + .copy_from_slice(&header.to_bytes(false)); + + storage.flush()?; + // Write the magic number only after the data structure is initialized and written to disk + // to ensure that it's crash safe + storage + .write(0, DB_HEADER_SIZE, true)? + .mem_mut() + .copy_from_slice(&header.to_bytes(true)); + storage.flush()?; + } + let header_bytes = storage.read_direct(0, DB_HEADER_SIZE)?; + let (mut header, repair_info) = DatabaseHeader::from_bytes(&header_bytes)?; + + assert_eq!(header.page_size() as usize, page_size); + assert!(storage.raw_file_len()? >= header.layout().len()); + let needs_recovery = + header.recovery_required || header.layout().len() != storage.raw_file_len()?; + if needs_recovery { + if read_only { + return Err(DatabaseError::RepairAborted); + } + let layout = header.layout(); + let region_max_pages = layout.full_region_layout().num_pages(); + let region_header_pages = layout.full_region_layout().get_header_pages(); + header.set_layout(DatabaseLayout::recalculate( + storage.raw_file_len()?, + region_header_pages, + region_max_pages, + page_size.try_into().unwrap(), + )); + header.pick_primary_for_repair(repair_info)?; + assert!(!repair_info.invalid_magic_number); + storage + .write(0, DB_HEADER_SIZE, true)? + .mem_mut() + .copy_from_slice(&header.to_bytes(true)); + storage.flush()?; + } + + let layout = header.layout(); + assert_eq!(layout.len(), storage.raw_file_len()?); + let region_size = layout.full_region_layout().len(); + let region_header_size = layout.full_region_layout().data_section().start; + let state = InMemoryState::new(header); + + assert!(page_size >= DB_HEADER_SIZE); + + Ok(Self { + allocated_since_commit: Mutex::new(Default::default()), + unpersisted: Mutex::new(Default::default()), + needs_recovery: AtomicBool::new(needs_recovery), + storage, + state: Mutex::new(state), + #[cfg(debug_assertions)] + open_dirty_pages: Arc::new(Mutex::new(HashSet::new())), + #[cfg(debug_assertions)] + read_page_ref_counts: Arc::new(Mutex::new(HashMap::new())), + #[cfg(debug_assertions)] + allocated_pages: Arc::new(Mutex::new(Default::default())), + read_from_secondary: AtomicBool::new(false), + page_size: page_size.try_into().unwrap(), + region_size, + region_header_with_padding_size: region_header_size, + }) + } + + pub(crate) fn cache_stats(&self) -> CacheStats { + self.storage.cache_stats() + } + + pub(crate) fn check_io_errors(&self) -> Result { + self.storage.check_io_errors() + } + + #[cfg(debug_assertions)] + pub(crate) fn mark_debug_allocated_page(&self, page: PageNumber) { + assert!(self.allocated_pages.lock().unwrap().insert(page)); + } + + #[cfg(debug_assertions)] + pub(crate) fn all_allocated_pages(&self) -> Vec { + self.allocated_pages + .lock() + .unwrap() + .iter() + .copied() + .collect() + } + + #[cfg(debug_assertions)] + pub(crate) fn debug_check_allocator_consistency(&self) { + let state = self.state.lock().unwrap(); + let mut region_pages = vec![vec![]; state.allocators.region_allocators.len()]; + for p in self.allocated_pages.lock().unwrap().iter() { + region_pages[p.region as usize].push(*p); + } + for (i, allocator) in state.allocators.region_allocators.iter().enumerate() { + allocator.check_allocated_pages(i.try_into().unwrap(), ®ion_pages[i]); + } + } + + pub(crate) fn clear_read_cache(&self) { + self.storage.invalidate_cache_all(); + } + + pub(crate) fn clear_cache_and_reload(&mut self) -> Result { + assert!(self.allocated_since_commit.lock().unwrap().is_empty()); + + self.storage.flush()?; + self.storage.invalidate_cache_all(); + + let header_bytes = self.storage.read_direct(0, DB_HEADER_SIZE)?; + let (mut header, repair_info) = DatabaseHeader::from_bytes(&header_bytes)?; + // TODO: This ends up always being true because this is called from check_integrity() once the db is already open + // TODO: Also we should recheck the layout + let mut was_clean = true; + if header.recovery_required { + if !header.pick_primary_for_repair(repair_info)? { + was_clean = false; + } + if repair_info.invalid_magic_number { + return Err(StorageError::Corrupted("Invalid magic number".to_string()).into()); + } + self.storage + .write(0, DB_HEADER_SIZE, true)? + .mem_mut() + .copy_from_slice(&header.to_bytes(true)); + self.storage.flush()?; + } + + self.needs_recovery + .store(header.recovery_required, Ordering::Release); + self.state.lock().unwrap().header = header; + + Ok(was_clean) + } + + pub(crate) fn begin_writable(&self) -> Result { + let mut state = self.state.lock().unwrap(); + assert!(!state.header.recovery_required); + state.header.recovery_required = true; + self.write_header(&state.header)?; + self.storage.flush() + } + + pub(crate) fn used_two_phase_commit(&self) -> bool { + self.state.lock().unwrap().header.two_phase_commit + } + + pub(crate) fn allocator_hash(&self) -> u128 { + self.state.lock().unwrap().allocators.xxh3_hash() + } + + // TODO: need a clearer distinction between this and needs_repair() + pub(crate) fn storage_failure(&self) -> bool { + self.needs_recovery.load(Ordering::Acquire) + } + + pub(crate) fn repair_primary_corrupted(&self) { + let mut state = self.state.lock().unwrap(); + state.header.swap_primary_slot(); + } + + pub(crate) fn begin_repair(&self) -> Result<()> { + let mut state = self.state.lock().unwrap(); + state.allocators = Allocators::new(state.header.layout()); + #[cfg(debug_assertions)] + self.allocated_pages.lock().unwrap().clear(); + + Ok(()) + } + + pub(crate) fn mark_page_allocated(&self, page_number: PageNumber) { + let mut state = self.state.lock().unwrap(); + let region_index = page_number.region; + let allocator = state.get_region_mut(region_index); + allocator.record_alloc(page_number.page_index, page_number.page_order); + #[cfg(debug_assertions)] + assert!(self.allocated_pages.lock().unwrap().insert(page_number)); + } + + fn write_header(&self, header: &DatabaseHeader) -> Result { + self.storage + .write(0, DB_HEADER_SIZE, true)? + .mem_mut() + .copy_from_slice(&header.to_bytes(true)); + + Ok(()) + } + + pub(crate) fn end_repair(&self) -> Result<()> { + let mut state = self.state.lock().unwrap(); + state.header.recovery_required = false; + self.write_header(&state.header)?; + let result = self.storage.flush(); + self.needs_recovery.store(false, Ordering::Release); + + result + } + + pub(crate) fn reserve_allocator_state( + &self, + tree: &mut AllocatorStateTreeMut, + transaction_id: TransactionId, + ) -> Result { + let state = self.state.lock().unwrap(); + let layout = state.header.layout(); + let num_regions = layout.num_regions(); + let region_tracker_len = state.allocators.region_tracker.to_vec().len(); + let region_lens: Vec = state + .allocators + .region_allocators + .iter() + .map(|x| x.to_vec().len()) + .collect(); + drop(state); + + for i in 0..num_regions { + let region_bytes_len = region_lens[i as usize]; + tree.insert( + &AllocatorStateKey::Region(i), + &vec![0; region_bytes_len].as_ref(), + )?; + } + + tree.insert( + &AllocatorStateKey::RegionTracker, + &vec![0; region_tracker_len].as_ref(), + )?; + + tree.insert( + &AllocatorStateKey::TransactionId, + &transaction_id.raw_id().to_le_bytes().as_ref(), + )?; + + Ok(num_regions) + } + + // Returns true on success, or false if the number of regions has changed + pub(crate) fn try_save_allocator_state( + &self, + tree: &mut AllocatorStateTreeMut, + num_regions: u32, + ) -> Result { + // Has the number of regions changed since reserve_allocator_state() was called? + let state = self.state.lock().unwrap(); + if num_regions != state.header.layout().num_regions() { + return Ok(false); + } + + for i in 0..num_regions { + let region_bytes = &state.allocators.region_allocators[i as usize].to_vec(); + if tree + .get(&AllocatorStateKey::Region(i))? + .unwrap() + .value() + .len() + < region_bytes.len() + { + // The allocator state grew too much since we reserved space + return Ok(false); + } + tree.insert_inplace(&AllocatorStateKey::Region(i), ®ion_bytes.as_ref())?; + } + + let region_tracker_bytes = state.allocators.region_tracker.to_vec(); + if tree + .get(&AllocatorStateKey::RegionTracker)? + .unwrap() + .value() + .len() + < region_tracker_bytes.len() + { + // The allocator state grew too much since we reserved space + return Ok(false); + } + tree.insert_inplace( + &AllocatorStateKey::RegionTracker, + ®ion_tracker_bytes.as_ref(), + )?; + + Ok(true) + } + + // Returns true if the allocator state table is up to date, or false if it's stale + pub(crate) fn is_valid_allocator_state(&self, tree: &AllocatorStateTree) -> Result { + // See if this is stale allocator state left over from a previous transaction. That won't + // happen during normal operation, since WriteTransaction::commit() always updates the + // allocator state table before calling TransactionalMemory::commit(), but there are also + // a few places where TransactionalMemory::commit() is called directly without using a + // WriteTransaction. When that happens, any existing allocator state table will be left + // in place but is no longer valid. (And even if there were no such calls today, it would + // be an easy mistake to make! So it's good that we check.) + let Some(value) = tree.get(&AllocatorStateKey::TransactionId)? else { + return Ok(false); + }; + let transaction_id = + TransactionId::new(u64::from_le_bytes(value.value().try_into().unwrap())); + + Ok(transaction_id == self.get_last_committed_transaction_id()?) + } + + pub(crate) fn load_allocator_state(&self, tree: &AllocatorStateTree) -> Result { + assert!(self.is_valid_allocator_state(tree)?); + + // Load the allocator state + let mut region_allocators = vec![]; + for region in + tree.range(&(AllocatorStateKey::Region(0)..=AllocatorStateKey::Region(u32::MAX)))? + { + region_allocators.push(BuddyAllocator::from_bytes(region?.value())); + } + + let region_tracker = RegionTracker::from_bytes( + tree.get(&AllocatorStateKey::RegionTracker)? + .unwrap() + .value(), + ); + + let mut state = self.state.lock().unwrap(); + state.allocators = Allocators { + region_tracker, + region_allocators, + }; + + // Resize the allocators to match the current file size + let layout = state.header.layout(); + state.allocators.resize_to(layout); + drop(state); + + self.state.lock().unwrap().header.recovery_required = false; + self.needs_recovery.store(false, Ordering::Release); + + Ok(()) + } + + #[cfg_attr(not(debug_assertions), expect(unused_variables))] + pub(crate) fn is_allocated(&self, page: PageNumber) -> bool { + #[cfg(debug_assertions)] + { + let allocated = self.allocated_pages.lock().unwrap(); + allocated.contains(&page) + } + #[cfg(not(debug_assertions))] + { + unreachable!() + } + } + + // Commit all outstanding changes and make them visible as the primary + #[allow(clippy::too_many_arguments)] + pub(crate) fn commit( + &self, + data_root: Option, + system_root: Option, + transaction_id: TransactionId, + two_phase: bool, + shrink_policy: ShrinkPolicy, + ) -> Result { + let result = self.commit_inner( + data_root, + system_root, + transaction_id, + two_phase, + shrink_policy, + ); + if result.is_err() { + self.needs_recovery.store(true, Ordering::Release); + } + result + } + + #[allow(clippy::too_many_arguments)] + fn commit_inner( + &self, + data_root: Option, + system_root: Option, + transaction_id: TransactionId, + two_phase: bool, + shrink_policy: ShrinkPolicy, + ) -> Result { + // All mutable pages must be dropped, this ensures that when a transaction completes + // no more writes can happen to the pages it allocated. Thus it is safe to make them visible + // to future read transactions + #[cfg(debug_assertions)] + debug_assert!(self.open_dirty_pages.lock().unwrap().is_empty()); + assert!(!self.needs_recovery.load(Ordering::Acquire)); + + let mut state = self.state.lock().unwrap(); + // Trim surplus file space, before finalizing the commit + let shrunk = if !matches!(shrink_policy, ShrinkPolicy::Never) { + Self::try_shrink(&mut state, matches!(shrink_policy, ShrinkPolicy::Maximum))? + } else { + false + }; + // Copy the header so that we can release the state lock, while we flush the file + let mut header = state.header.clone(); + drop(state); + + let old_transaction_id = header.secondary_slot().transaction_id; + let secondary = header.secondary_slot_mut(); + secondary.transaction_id = transaction_id; + secondary.user_root = data_root; + secondary.system_root = system_root; + + self.write_header(&header)?; + + // Use 2-phase commit, if checksums are disabled + if two_phase { + self.storage.flush()?; + } + + // Make our new commit the primary, and record whether it was a 2-phase commit. + // These two bits need to be written atomically + header.swap_primary_slot(); + header.two_phase_commit = two_phase; + + // Write the new header to disk + self.write_header(&header)?; + self.storage.flush()?; + + if shrunk { + let result = self.storage.resize(header.layout().len()); + if result.is_err() { + // TODO: it would be nice to have a more cohesive approach to setting this. + // we do it in commit() & rollback() on failure, but there are probably other places that need it + self.needs_recovery.store(true, Ordering::Release); + return result; + } + } + let mut allocated_since_commit = self.allocated_since_commit.lock().unwrap(); + allocated_since_commit.clear(); + allocated_since_commit.shrink_to_fit(); + let mut unpersisted = self.unpersisted.lock().unwrap(); + unpersisted.clear(); + unpersisted.shrink_to_fit(); + + let mut state = self.state.lock().unwrap(); + assert_eq!( + state.header.secondary_slot().transaction_id, + old_transaction_id + ); + state.header = header; + self.read_from_secondary.store(false, Ordering::Release); + // Hold lock until read_from_secondary is set to false, so that the new primary state is read. + // TODO: maybe we can remove the whole read_from_secondary flag? + drop(state); + + Ok(()) + } + + // Make changes visible, without a durability guarantee + pub(crate) fn non_durable_commit( + &self, + data_root: Option, + system_root: Option, + transaction_id: TransactionId, + ) -> Result { + // All mutable pages must be dropped, this ensures that when a transaction completes + // no more writes can happen to the pages it allocated. Thus it is safe to make them visible + // to future read transactions + #[cfg(debug_assertions)] + debug_assert!(self.open_dirty_pages.lock().unwrap().is_empty()); + assert!(!self.needs_recovery.load(Ordering::Acquire)); + + let mut unpersisted = self.unpersisted.lock().unwrap(); + let mut allocated_since_commit = self.allocated_since_commit.lock().unwrap(); + unpersisted.extend(allocated_since_commit.drain()); + allocated_since_commit.shrink_to_fit(); + self.storage.write_barrier()?; + + let mut state = self.state.lock().unwrap(); + let secondary = state.header.secondary_slot_mut(); + secondary.transaction_id = transaction_id; + secondary.user_root = data_root; + secondary.system_root = system_root; + + // TODO: maybe we can remove this flag and just update the in-memory DatabaseHeader state? + self.read_from_secondary.store(true, Ordering::Release); + + Ok(()) + } + + pub(crate) fn rollback_uncommitted_writes(&self) -> Result { + let result = self.rollback_uncommitted_writes_inner(); + if result.is_err() { + self.needs_recovery.store(true, Ordering::Release); + } + result + } + + fn rollback_uncommitted_writes_inner(&self) -> Result { + #[cfg(debug_assertions)] + { + let dirty_pages = self.open_dirty_pages.lock().unwrap(); + debug_assert!( + dirty_pages.is_empty(), + "Dirty pages outstanding: {dirty_pages:?}" + ); + } + assert!(!self.needs_recovery.load(Ordering::Acquire)); + let mut state = self.state.lock().unwrap(); + let mut guard = self.allocated_since_commit.lock().unwrap(); + for page_number in guard.iter() { + let region_index = page_number.region; + state + .get_region_tracker_mut() + .mark_free(page_number.page_order, region_index); + state + .get_region_mut(region_index) + .free(page_number.page_index, page_number.page_order); + #[cfg(debug_assertions)] + assert!(self.allocated_pages.lock().unwrap().remove(page_number)); + + let address = page_number.address_range( + self.page_size.into(), + self.region_size, + self.region_header_with_padding_size, + self.page_size, + ); + let len: usize = (address.end - address.start).try_into().unwrap(); + self.storage.invalidate_cache(address.start, len); + self.storage.cancel_pending_write(address.start, len); + } + guard.clear(); + guard.shrink_to_fit(); + + Ok(()) + } + + pub(crate) fn get_page(&self, page_number: PageNumber, hint: PageHint) -> Result { + let range = page_number.address_range( + self.page_size.into(), + self.region_size, + self.region_header_with_padding_size, + self.page_size, + ); + let len: usize = (range.end - range.start).try_into().unwrap(); + let mem = self.storage.read(range.start, len, hint)?; + + // We must not retrieve an immutable reference to a page which already has a mutable ref to it + #[cfg(debug_assertions)] + { + let dirty_pages = self.open_dirty_pages.lock().unwrap(); + debug_assert!(!dirty_pages.contains(&page_number), "{page_number:?}"); + *(self + .read_page_ref_counts + .lock() + .unwrap() + .entry(page_number) + .or_default()) += 1; + drop(dirty_pages); + } + + Ok(PageImpl { + mem, + page_number, + #[cfg(debug_assertions)] + open_pages: self.read_page_ref_counts.clone(), + }) + } + + // NOTE: the caller must ensure that the read cache has been invalidated or stale reads my occur + pub(crate) fn get_page_mut<'txn>(&self, page_number: PageNumber) -> Result> { + #[cfg(debug_assertions)] + { + assert!( + !self + .read_page_ref_counts + .lock() + .unwrap() + .contains_key(&page_number) + ); + assert!(!self.open_dirty_pages.lock().unwrap().contains(&page_number)); + } + + let address_range = page_number.address_range( + self.page_size.into(), + self.region_size, + self.region_header_with_padding_size, + self.page_size, + ); + let len: usize = (address_range.end - address_range.start) + .try_into() + .unwrap(); + let mem = self.storage.write(address_range.start, len, false)?; + + #[cfg(debug_assertions)] + { + assert!(self.open_dirty_pages.lock().unwrap().insert(page_number)); + } + + Ok(PageMut { + mem, + page_number, + _lifetime: PhantomData, + #[cfg(debug_assertions)] + open_pages: self.open_dirty_pages.clone(), + }) + } + + pub(crate) fn get_version(&self) -> u8 { + let state = self.state.lock().unwrap(); + if self.read_from_secondary.load(Ordering::Acquire) { + state.header.secondary_slot().version + } else { + state.header.primary_slot().version + } + } + + pub(crate) fn get_data_root(&self) -> Option { + let state = self.state.lock().unwrap(); + if self.read_from_secondary.load(Ordering::Acquire) { + state.header.secondary_slot().user_root + } else { + state.header.primary_slot().user_root + } + } + + pub(crate) fn get_system_root(&self) -> Option { + let state = self.state.lock().unwrap(); + if self.read_from_secondary.load(Ordering::Acquire) { + state.header.secondary_slot().system_root + } else { + state.header.primary_slot().system_root + } + } + + pub(crate) fn get_last_committed_transaction_id(&self) -> Result { + let state = self.state.lock()?; + if self.read_from_secondary.load(Ordering::Acquire) { + Ok(state.header.secondary_slot().transaction_id) + } else { + Ok(state.header.primary_slot().transaction_id) + } + } + + pub(crate) fn get_last_durable_transaction_id(&self) -> Result { + let state = self.state.lock()?; + Ok(state.header.primary_slot().transaction_id) + } + + pub(crate) fn free(&self, page: PageNumber, allocated: &mut PageTrackerPolicy) { + self.allocated_since_commit.lock().unwrap().remove(&page); + self.free_helper(page, allocated); + } + + fn free_helper(&self, page: PageNumber, allocated: &mut PageTrackerPolicy) { + #[cfg(debug_assertions)] + { + assert!( + !self + .read_page_ref_counts + .lock() + .unwrap() + .contains_key(&page) + ); + assert!(self.allocated_pages.lock().unwrap().remove(&page)); + assert!(!self.open_dirty_pages.lock().unwrap().contains(&page)); + } + allocated.remove(page); + let mut state = self.state.lock().unwrap(); + let region_index = page.region; + // Free in the regional allocator + state + .get_region_mut(region_index) + .free(page.page_index, page.page_order); + // Ensure that the region is marked as having free space + state + .get_region_tracker_mut() + .mark_free(page.page_order, region_index); + + let address_range = page.address_range( + self.page_size.into(), + self.region_size, + self.region_header_with_padding_size, + self.page_size, + ); + let len: usize = (address_range.end - address_range.start) + .try_into() + .unwrap(); + self.storage.invalidate_cache(address_range.start, len); + self.storage.cancel_pending_write(address_range.start, len); + } + + // Frees the page if no durable commit has occurred, since it was allocated. Returns true, if the page was freed + pub(crate) fn free_if_unpersisted( + &self, + page: PageNumber, + allocated: &mut PageTrackerPolicy, + ) -> bool { + if self.unpersisted.lock().unwrap().remove(&page) { + self.free_helper(page, allocated); + true + } else { + false + } + } + + // Frees the page if it was allocated since the last commit. Returns true, if the page was freed + pub(crate) fn free_if_uncommitted( + &self, + page: PageNumber, + allocated: &mut PageTrackerPolicy, + ) -> bool { + if self.allocated_since_commit.lock().unwrap().remove(&page) { + self.free_helper(page, allocated); + true + } else { + false + } + } + + // Page has not been committed + pub(crate) fn uncommitted(&self, page: PageNumber) -> bool { + self.allocated_since_commit.lock().unwrap().contains(&page) + } + + pub(crate) fn unpersisted(&self, page: PageNumber) -> bool { + self.unpersisted.lock().unwrap().contains(&page) + } + + pub(crate) fn allocate_helper<'txn>( + &self, + allocation_size: usize, + lowest: bool, + transactional: bool, + ) -> Result> { + let required_pages = allocation_size.div_ceil(self.get_page_size()); + let required_order = ceil_log2(required_pages); + + let mut state = self.state.lock().unwrap(); + + let page_number = if let Some(page_number) = + Self::allocate_helper_retry(&mut state, required_order, lowest)? + { + page_number + } else { + self.grow(&mut state, required_order)?; + Self::allocate_helper_retry(&mut state, required_order, lowest)?.unwrap() + }; + + #[cfg(debug_assertions)] + { + assert!(self.allocated_pages.lock().unwrap().insert(page_number)); + assert!( + !self + .read_page_ref_counts + .lock() + .unwrap() + .contains_key(&page_number), + "Allocated a page that is still referenced! {page_number:?}" + ); + assert!(!self.open_dirty_pages.lock().unwrap().contains(&page_number)); + } + + if transactional { + self.allocated_since_commit + .lock() + .unwrap() + .insert(page_number); + } + + let address_range = page_number.address_range( + self.page_size.into(), + self.region_size, + self.region_header_with_padding_size, + self.page_size, + ); + let len: usize = (address_range.end - address_range.start) + .try_into() + .unwrap(); + + #[allow(unused_mut)] + let mut mem = self.storage.write(address_range.start, len, true)?; + debug_assert!(mem.mem().len() >= allocation_size); + + #[cfg(debug_assertions)] + { + assert!(self.open_dirty_pages.lock().unwrap().insert(page_number)); + + // Poison the memory in debug mode to help detect uninitialized reads + mem.mem_mut().fill(0xFF); + } + + Ok(PageMut { + mem, + page_number, + _lifetime: PhantomData, + #[cfg(debug_assertions)] + open_pages: self.open_dirty_pages.clone(), + }) + } + + fn allocate_helper_retry( + state: &mut InMemoryState, + required_order: u8, + lowest: bool, + ) -> Result> { + loop { + let Some(candidate_region) = state.get_region_tracker_mut().find_free(required_order) + else { + return Ok(None); + }; + let region = state.get_region_mut(candidate_region); + let r = if lowest { + region.alloc_lowest(required_order) + } else { + region.alloc(required_order) + }; + if let Some(page) = r { + return Ok(Some(PageNumber::new( + candidate_region, + page, + required_order, + ))); + } + // Mark the region, if it's full + state + .get_region_tracker_mut() + .mark_full(required_order, candidate_region); + } + } + + fn try_shrink(state: &mut InMemoryState, force: bool) -> Result { + let layout = state.header.layout(); + let last_region_index = layout.num_regions() - 1; + let last_allocator = state.get_region(last_region_index); + let trailing_free = last_allocator.trailing_free_pages(); + let last_allocator_len = last_allocator.len(); + if trailing_free == 0 { + return Ok(false); + } + if trailing_free < last_allocator_len / 2 && !force { + return Ok(false); + } + let reduce_by = if layout.num_regions() > 1 && trailing_free == last_allocator_len { + trailing_free + } else if force { + // Do not shrink the database to zero size + min(last_allocator_len - 1, trailing_free) + } else { + trailing_free / 2 + }; + + let mut new_layout = layout; + new_layout.reduce_last_region(reduce_by); + state.allocators.resize_to(new_layout); + assert!(new_layout.len() <= layout.len()); + state.header.set_layout(new_layout); + + Ok(true) + } + + fn grow(&self, state: &mut InMemoryState, required_order_allocation: u8) -> Result<()> { + let layout = state.header.layout(); + let required_growth = + 2u64.pow(required_order_allocation.into()) * u64::from(state.header.page_size()); + let max_region_size = u64::from(state.header.layout().full_region_layout().num_pages()) + * u64::from(state.header.page_size()); + let next_desired_size = if layout.num_full_regions() > 0 { + if let Some(trailing) = layout.trailing_region_layout() { + if 2 * required_growth < max_region_size - trailing.usable_bytes() { + // Fill out the trailing region + layout.usable_bytes() + (max_region_size - trailing.usable_bytes()) + } else { + // Fill out trailing & Grow by 1 region + layout.usable_bytes() + 2 * max_region_size - trailing.usable_bytes() + } + } else { + // Grow by 1 region + layout.usable_bytes() + max_region_size + } + } else { + max( + layout.usable_bytes() * 2, + layout.usable_bytes() + required_growth * 2, + ) + }; + let new_layout = DatabaseLayout::calculate( + next_desired_size, + state.header.layout().full_region_layout().num_pages(), + state + .header + .layout() + .full_region_layout() + .get_header_pages(), + self.page_size, + ); + assert!(new_layout.len() >= layout.len()); + + let result = self.storage.resize(new_layout.len()); + if result.is_err() { + // TODO: it would be nice to have a more cohesive approach to setting this. + // we do it in commit() & rollback() on failure, but there are probably other places that need it + self.needs_recovery.store(true, Ordering::Release); + return result; + } + + state.allocators.resize_to(new_layout); + state.header.set_layout(new_layout); + Ok(()) + } + + pub(crate) fn allocate<'txn>( + &self, + allocation_size: usize, + allocated: &mut PageTrackerPolicy, + ) -> Result> { + let result = self.allocate_helper(allocation_size, false, true); + if let Ok(ref page) = result { + allocated.insert(page.get_page_number()); + } + result + } + + pub(crate) fn allocate_lowest<'txn>(&self, allocation_size: usize) -> Result> { + self.allocate_helper(allocation_size, true, true) + } + + pub(crate) fn count_allocated_pages(&self) -> Result { + let state = self.state.lock().unwrap(); + let mut count = 0u64; + for i in 0..state.header.layout().num_regions() { + count += u64::from(state.get_region(i).count_allocated_pages()); + } + + Ok(count) + } + + pub(crate) fn count_free_pages(&self) -> Result { + let state = self.state.lock().unwrap(); + let mut count = 0u64; + for i in 0..state.header.layout().num_regions() { + count += u64::from(state.get_region(i).count_free_pages()); + } + + Ok(count) + } + + pub(crate) fn get_page_size(&self) -> usize { + self.page_size.try_into().unwrap() + } + + pub(crate) fn close(&self) -> Result { + if !self.needs_recovery.load(Ordering::Acquire) && !thread::panicking() { + let mut state = self.state.lock()?; + if self.storage.flush().is_ok() { + state.header.recovery_required = false; + self.write_header(&state.header)?; + self.storage.flush()?; + } + } + + self.storage.close()?; + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::tree_store::page_store::page_manager::INITIAL_REGIONS; + use crate::{Database, TableDefinition}; + + // Test that the region tracker expansion code works, by adding more data than fits into the initial max regions + #[test] + fn out_of_regions() { + let tmpfile = crate::create_tempfile(); + let table_definition: TableDefinition = TableDefinition::new("x"); + let page_size = 1024; + let big_value = vec![0u8; 5 * page_size]; + + let db = Database::builder() + .set_region_size((8 * page_size).try_into().unwrap()) + .set_page_size(page_size) + .create(tmpfile.path()) + .unwrap(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(table_definition).unwrap(); + for i in 0..=INITIAL_REGIONS { + table.insert(&i, big_value.as_slice()).unwrap(); + } + } + txn.commit().unwrap(); + drop(db); + + let mut db = Database::builder() + .set_region_size((8 * page_size).try_into().unwrap()) + .set_page_size(page_size) + .open(tmpfile.path()) + .unwrap(); + assert!(db.check_integrity().unwrap()); + } + + // Make sure the database remains consistent after a panic + #[test] + #[cfg(panic = "unwind")] + fn panic() { + let tmpfile = crate::create_tempfile(); + let table_definition: TableDefinition = TableDefinition::new("x"); + + let _ = std::panic::catch_unwind(|| { + let db = Database::create(&tmpfile).unwrap(); + let txn = db.begin_write().unwrap(); + txn.open_table(table_definition).unwrap(); + panic!(); + }); + + let mut db = Database::open(tmpfile).unwrap(); + assert!(db.check_integrity().unwrap()); + } +} diff --git a/v2/redb/src/tree_store/page_store/region.rs b/v2/redb/src/tree_store/page_store/region.rs new file mode 100644 index 000000000..b7f716367 --- /dev/null +++ b/v2/redb/src/tree_store/page_store/region.rs @@ -0,0 +1,203 @@ +use crate::tree_store::page_store::base::MAX_REGIONS; +use crate::tree_store::page_store::bitmap::BtreeBitmap; +use crate::tree_store::page_store::buddy_allocator::BuddyAllocator; +use crate::tree_store::page_store::layout::DatabaseLayout; +use crate::tree_store::page_store::page_manager::{INITIAL_REGIONS, MAX_MAX_PAGE_ORDER}; +use std::cmp::{self, max}; +use std::mem::size_of; + +// Tracks the page orders that MAY BE free in each region. This data structure is optimistic, so +// a region may not actually have a page free for a given order +pub(crate) struct RegionTracker { + order_trackers: Vec, +} + +impl RegionTracker { + pub(crate) fn new(regions: u32, orders: u8) -> Self { + let mut data = vec![]; + for _ in 0..orders { + data.push(BtreeBitmap::new_padded(regions, regions, MAX_REGIONS)); + } + Self { + order_trackers: data, + } + } + + // Format: + // num_orders: u32 number of order allocators + // allocator_lens: u32 length of each allocator + // data: BtreeBitmap data for each order + pub(super) fn to_vec(&self) -> Vec { + let mut result = vec![]; + let orders: u32 = self.order_trackers.len().try_into().unwrap(); + let allocator_lens: Vec = self + .order_trackers + .iter() + .map(|x| x.to_vec().len().try_into().unwrap()) + .collect(); + result.extend(orders.to_le_bytes()); + for allocator_len in allocator_lens { + result.extend(allocator_len.to_le_bytes()); + } + for order in &self.order_trackers { + result.extend(&order.to_vec()); + } + result + } + + pub(super) fn from_bytes(page: &[u8]) -> Self { + let orders = u32::from_le_bytes(page[..size_of::()].try_into().unwrap()); + let mut start = size_of::(); + let mut allocator_lens = vec![]; + for _ in 0..orders { + let allocator_len = + u32::from_le_bytes(page[start..start + size_of::()].try_into().unwrap()) + as usize; + allocator_lens.push(allocator_len); + start += size_of::(); + } + let mut data = vec![]; + for allocator_len in allocator_lens { + data.push(BtreeBitmap::from_bytes( + &page[start..(start + allocator_len)], + )); + start += allocator_len; + } + + Self { + order_trackers: data, + } + } + + pub(crate) fn find_free(&self, order: u8) -> Option { + self.order_trackers[order as usize].find_first_unset() + } + + pub(crate) fn mark_free(&mut self, order: u8, region: u32) { + let order: usize = order.into(); + for i in 0..=order { + self.order_trackers[i].clear(region); + } + } + + pub(crate) fn mark_full(&mut self, order: u8, region: u32) { + let order: usize = order.into(); + assert!(order < self.order_trackers.len()); + for i in order..self.order_trackers.len() { + self.order_trackers[i].set(region); + } + } + + fn resize(&mut self, new_capacity: u32) { + for order in &mut self.order_trackers { + order.resize(new_capacity, true); + } + } + + fn len(&self) -> u32 { + self.order_trackers[0].len() + } +} + +pub(super) struct Allocators { + pub(super) region_tracker: RegionTracker, + pub(super) region_allocators: Vec, +} + +impl Allocators { + pub(super) fn new(layout: DatabaseLayout) -> Self { + let mut region_allocators = vec![]; + let initial_regions = max(INITIAL_REGIONS, layout.num_regions()); + let mut region_tracker = RegionTracker::new(initial_regions, MAX_MAX_PAGE_ORDER + 1); + for i in 0..layout.num_regions() { + let region_layout = layout.region_layout(i); + let allocator = BuddyAllocator::new( + region_layout.num_pages(), + layout.full_region_layout().num_pages(), + ); + let max_order = allocator.get_max_order(); + region_tracker.mark_free(max_order, i); + region_allocators.push(allocator); + } + + Self { + region_tracker, + region_allocators, + } + } + + pub(crate) fn xxh3_hash(&self) -> u128 { + // Ignore the region tracker because it is an optimistic cache, and so may not match + // between repairs of the allocators + let mut result = 0; + for allocator in &self.region_allocators { + result ^= allocator.xxh3_hash(); + } + result + } + + pub(super) fn resize_to(&mut self, new_layout: DatabaseLayout) { + let shrink = match (new_layout.num_regions() as usize).cmp(&self.region_allocators.len()) { + cmp::Ordering::Less => true, + cmp::Ordering::Equal => { + let allocator = self.region_allocators.last().unwrap(); + let last_region = new_layout + .trailing_region_layout() + .unwrap_or_else(|| new_layout.full_region_layout()); + match last_region.num_pages().cmp(&allocator.len()) { + cmp::Ordering::Less => true, + cmp::Ordering::Equal => { + // No-op + return; + } + cmp::Ordering::Greater => false, + } + } + cmp::Ordering::Greater => false, + }; + + if shrink { + // Drop all regions that were removed + for i in new_layout.num_regions()..(self.region_allocators.len().try_into().unwrap()) { + self.region_tracker.mark_full(0, i); + } + self.region_allocators + .drain((new_layout.num_regions() as usize)..); + + // Resize the last region + let last_region = new_layout + .trailing_region_layout() + .unwrap_or_else(|| new_layout.full_region_layout()); + let allocator = self.region_allocators.last_mut().unwrap(); + if allocator.len() > last_region.num_pages() { + allocator.resize(last_region.num_pages()); + } + } else { + let old_num_regions = self.region_allocators.len(); + for i in 0..new_layout.num_regions() { + let new_region = new_layout.region_layout(i); + if (i as usize) < old_num_regions { + let allocator = &mut self.region_allocators[i as usize]; + assert!(new_region.num_pages() >= allocator.len()); + if new_region.num_pages() != allocator.len() { + allocator.resize(new_region.num_pages()); + let highest_free = allocator.highest_free_order().unwrap(); + self.region_tracker.mark_free(highest_free, i); + } + } else { + // brand new region + let allocator = BuddyAllocator::new( + new_region.num_pages(), + new_layout.full_region_layout().num_pages(), + ); + let highest_free = allocator.highest_free_order().unwrap(); + if i >= self.region_tracker.len() { + self.region_tracker.resize(i + 1); + } + self.region_tracker.mark_free(highest_free, i); + self.region_allocators.push(allocator); + } + } + } + } +} diff --git a/v2/redb/src/tree_store/page_store/savepoint.rs b/v2/redb/src/tree_store/page_store/savepoint.rs new file mode 100644 index 000000000..520afb472 --- /dev/null +++ b/v2/redb/src/tree_store/page_store/savepoint.rs @@ -0,0 +1,195 @@ +use crate::transaction_tracker::{SavepointId, TransactionId, TransactionTracker}; +use crate::tree_store::page_store::page_manager::FILE_FORMAT_VERSION3; +use crate::tree_store::{BtreeHeader, TransactionalMemory}; +use crate::{TypeName, Value}; +use std::fmt::Debug; +use std::mem::size_of; +use std::sync::Arc; + +// on-disk format: +// * 1 byte: version +// * 8 bytes: savepoint id +// * 8 bytes: transaction id +// * 1 byte: user root not-null +// * 8 bytes: user root page +// * 8 bytes: user root checksum +/// A database savepoint +/// +/// May be used with [`WriteTransaction::restore_savepoint`] to restore the database to the state +/// when this savepoint was created +/// +/// [`WriteTransaction::restore_savepoint`]: crate::WriteTransaction::restore_savepoint +pub struct Savepoint { + version: u8, + id: SavepointId, + // Each savepoint has an associated read transaction id to ensure that any pages it references + // are not freed + transaction_id: TransactionId, + user_root: Option, + transaction_tracker: Arc, + ephemeral: bool, +} + +impl Savepoint { + #[allow(clippy::too_many_arguments)] + pub(crate) fn new_ephemeral( + mem: &TransactionalMemory, + transaction_tracker: Arc, + id: SavepointId, + transaction_id: TransactionId, + user_root: Option, + ) -> Self { + Self { + id, + transaction_id, + version: mem.get_version(), + user_root, + transaction_tracker, + ephemeral: true, + } + } + + pub(crate) fn get_version(&self) -> u8 { + self.version + } + + pub(crate) fn get_id(&self) -> SavepointId { + self.id + } + + pub(crate) fn get_transaction_id(&self) -> TransactionId { + self.transaction_id + } + + pub(crate) fn get_user_root(&self) -> Option { + self.user_root + } + + pub(crate) fn db_address(&self) -> *const TransactionTracker { + std::ptr::from_ref(self.transaction_tracker.as_ref()) + } + + pub(crate) fn set_persistent(&mut self) { + self.ephemeral = false; + } +} + +impl Drop for Savepoint { + fn drop(&mut self) { + if self.ephemeral { + self.transaction_tracker + .deallocate_savepoint(self.get_id(), self.get_transaction_id()); + } + } +} + +#[derive(Debug)] +pub(crate) enum SerializedSavepoint<'a> { + Ref(&'a [u8]), + Owned(Vec), +} + +impl SerializedSavepoint<'_> { + pub(crate) fn from_savepoint(savepoint: &Savepoint) -> Self { + assert_eq!(savepoint.version, FILE_FORMAT_VERSION3); + let mut result = vec![savepoint.version]; + result.extend(savepoint.id.0.to_le_bytes()); + result.extend(savepoint.transaction_id.raw_id().to_le_bytes()); + + if let Some(header) = savepoint.user_root { + result.push(1); + result.extend(header.to_le_bytes()); + } else { + result.push(0); + result.extend([0; BtreeHeader::serialized_size()]); + } + + Self::Owned(result) + } + + fn data(&self) -> &[u8] { + match self { + SerializedSavepoint::Ref(x) => x, + SerializedSavepoint::Owned(x) => x.as_slice(), + } + } + + pub(crate) fn to_savepoint(&self, transaction_tracker: Arc) -> Savepoint { + let data = self.data(); + let mut offset = 0; + let version = data[offset]; + assert_eq!(version, FILE_FORMAT_VERSION3); + offset += size_of::(); + + let id = u64::from_le_bytes( + data[offset..(offset + size_of::())] + .try_into() + .unwrap(), + ); + offset += size_of::(); + + let transaction_id = u64::from_le_bytes( + data[offset..(offset + size_of::())] + .try_into() + .unwrap(), + ); + offset += size_of::(); + + let not_null = data[offset]; + assert!(not_null == 0 || not_null == 1); + offset += 1; + let user_root = if not_null == 1 { + Some(BtreeHeader::from_le_bytes( + data[offset..(offset + BtreeHeader::serialized_size())] + .try_into() + .unwrap(), + )) + } else { + None + }; + offset += BtreeHeader::serialized_size(); + assert_eq!(offset, data.len()); + + Savepoint { + version, + id: SavepointId(id), + transaction_id: TransactionId::new(transaction_id), + user_root, + transaction_tracker, + ephemeral: false, + } + } +} + +impl Value for SerializedSavepoint<'_> { + type SelfType<'a> + = SerializedSavepoint<'a> + where + Self: 'a; + type AsBytes<'a> + = &'a [u8] + where + Self: 'a; + + fn fixed_width() -> Option { + None + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + SerializedSavepoint::Ref(data) + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'b, + { + value.data() + } + + fn type_name() -> TypeName { + TypeName::internal("redb::SerializedSavepoint") + } +} diff --git a/v2/redb/src/tree_store/page_store/xxh3.rs b/v2/redb/src/tree_store/page_store/xxh3.rs new file mode 100644 index 000000000..c943ffec1 --- /dev/null +++ b/v2/redb/src/tree_store/page_store/xxh3.rs @@ -0,0 +1,825 @@ +// Copyright (c) 2022 Christopher Berner +// +// MIT License +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// Copied from xxh3 crate, commit hash e91f09d1e930e179c11d5cfda6d14284bfb006f8 + +use std::mem::size_of; + +const STRIPE_LENGTH: usize = 64; +const SECRET_CONSUME_RATE: usize = 8; + +const MIN_SECRET_SIZE: usize = 136; +const DEFAULT_SECRET: [u8; 192] = [ + 0xb8, 0xfe, 0x6c, 0x39, 0x23, 0xa4, 0x4b, 0xbe, 0x7c, 0x01, 0x81, 0x2c, 0xf7, 0x21, 0xad, 0x1c, + 0xde, 0xd4, 0x6d, 0xe9, 0x83, 0x90, 0x97, 0xdb, 0x72, 0x40, 0xa4, 0xa4, 0xb7, 0xb3, 0x67, 0x1f, + 0xcb, 0x79, 0xe6, 0x4e, 0xcc, 0xc0, 0xe5, 0x78, 0x82, 0x5a, 0xd0, 0x7d, 0xcc, 0xff, 0x72, 0x21, + 0xb8, 0x08, 0x46, 0x74, 0xf7, 0x43, 0x24, 0x8e, 0xe0, 0x35, 0x90, 0xe6, 0x81, 0x3a, 0x26, 0x4c, + 0x3c, 0x28, 0x52, 0xbb, 0x91, 0xc3, 0x00, 0xcb, 0x88, 0xd0, 0x65, 0x8b, 0x1b, 0x53, 0x2e, 0xa3, + 0x71, 0x64, 0x48, 0x97, 0xa2, 0x0d, 0xf9, 0x4e, 0x38, 0x19, 0xef, 0x46, 0xa9, 0xde, 0xac, 0xd8, + 0xa8, 0xfa, 0x76, 0x3f, 0xe3, 0x9c, 0x34, 0x3f, 0xf9, 0xdc, 0xbb, 0xc7, 0xc7, 0x0b, 0x4f, 0x1d, + 0x8a, 0x51, 0xe0, 0x4b, 0xcd, 0xb4, 0x59, 0x31, 0xc8, 0x9f, 0x7e, 0xc9, 0xd9, 0x78, 0x73, 0x64, + 0xea, 0xc5, 0xac, 0x83, 0x34, 0xd3, 0xeb, 0xc3, 0xc5, 0x81, 0xa0, 0xff, 0xfa, 0x13, 0x63, 0xeb, + 0x17, 0x0d, 0xdd, 0x51, 0xb7, 0xf0, 0xda, 0x49, 0xd3, 0x16, 0x55, 0x26, 0x29, 0xd4, 0x68, 0x9e, + 0x2b, 0x16, 0xbe, 0x58, 0x7d, 0x47, 0xa1, 0xfc, 0x8f, 0xf8, 0xb8, 0xd1, 0x7a, 0xd0, 0x31, 0xce, + 0x45, 0xcb, 0x3a, 0x8f, 0x95, 0x16, 0x04, 0x28, 0xaf, 0xd7, 0xfb, 0xca, 0xbb, 0x4b, 0x40, 0x7e, +]; + +const PRIME32: [u64; 3] = [0x9E3779B1, 0x85EBCA77, 0xC2B2AE3D]; +const PRIME64: [u64; 5] = [ + 0x9E3779B185EBCA87, + 0xC2B2AE3D27D4EB4F, + 0x165667B19E3779F9, + 0x85EBCA77C2B2AE63, + 0x27D4EB2F165667C5, +]; + +const INIT_ACCUMULATORS: [u64; 8] = [ + PRIME32[2], PRIME64[0], PRIME64[1], PRIME64[2], PRIME64[3], PRIME32[1], PRIME64[4], PRIME32[0], +]; + +pub fn hash64_with_seed(data: &[u8], seed: u64) -> u64 { + if data.len() <= 240 { + hash64_0to240(data, &DEFAULT_SECRET, seed) + } else { + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + { + if is_x86_feature_detected!("avx2") { + unsafe { + return hash64_large_avx2(data, seed); + } + } + } + #[cfg(target_arch = "aarch64")] + { + unsafe { hash64_large_neon(data, seed) } + } + #[cfg(not(target_arch = "aarch64"))] + hash64_large_generic( + data, + seed, + gen_secret_generic, + scramble_accumulators_generic, + accumulate_stripe_generic, + ) + } +} + +pub fn hash128_with_seed(data: &[u8], seed: u64) -> u128 { + if data.len() <= 240 { + hash128_0to240(data, &DEFAULT_SECRET, seed) + } else { + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + if is_x86_feature_detected!("avx2") { + unsafe { + return hash128_large_avx2(data, seed); + } + } + #[cfg(target_arch = "aarch64")] + unsafe { + hash128_large_neon(data, seed) + } + #[cfg(not(target_arch = "aarch64"))] + hash128_large_generic( + data, + seed, + gen_secret_generic, + scramble_accumulators_generic, + accumulate_stripe_generic, + ) + } +} + +fn get_u32(data: &[u8], i: usize) -> u32 { + u32::from_le_bytes( + data[i * size_of::()..(i + 1) * size_of::()] + .try_into() + .unwrap(), + ) +} + +fn get_u64(data: &[u8], i: usize) -> u64 { + u64::from_le_bytes( + data[i * size_of::()..(i + 1) * size_of::()] + .try_into() + .unwrap(), + ) +} + +fn xxh64_avalanche(mut x: u64) -> u64 { + x ^= x >> 33; + x = x.wrapping_mul(PRIME64[1]); + x ^= x >> 29; + x = x.wrapping_mul(PRIME64[2]); + x ^= x >> 32; + x +} + +fn xxh3_avalanche(mut x: u64) -> u64 { + x = xorshift(x, 37); + x = x.wrapping_mul(0x165667919E3779F9); + x = xorshift(x, 32); + x +} + +#[inline(always)] +fn merge_accumulators( + accumulators: [u64; INIT_ACCUMULATORS.len()], + secret: &[u8], + init: u64, +) -> u64 { + let mut result = init; + for i in 0..4 { + let a1 = accumulators[2 * i]; + let a2 = accumulators[2 * i + 1]; + let s1 = get_u64(&secret[16 * i..], 0); + let s2 = get_u64(&secret[16 * i..], 1); + result = result.wrapping_add(mul128_and_xor(a1 ^ s1, a2 ^ s2)); + } + xxh3_avalanche(result) +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +#[target_feature(enable = "avx2")] +unsafe fn scramble_accumulators_avx2( + accumulators: &mut [u64; INIT_ACCUMULATORS.len()], + secret: &[u8], +) { + unsafe { + #[cfg(target_arch = "x86")] + use std::arch::x86::*; + #[cfg(target_arch = "x86_64")] + use std::arch::x86_64::*; + + #[allow(clippy::cast_possible_truncation)] + let simd_prime = _mm256_set1_epi32(PRIME32[0] as i32); + let secret_ptr = secret.as_ptr(); + let accumulators_ptr = accumulators.as_mut_ptr(); + + for i in 0..(STRIPE_LENGTH / 32) { + let a = _mm256_loadu_si256((accumulators_ptr as *const __m256i).add(i)); + let shifted = _mm256_srli_epi64::<47>(a); + let b = _mm256_xor_si256(a, shifted); + + let s = _mm256_loadu_si256((secret_ptr as *const __m256i).add(i)); + let c = _mm256_xor_si256(b, s); + let c_high = _mm256_shuffle_epi32::<49>(c); + + let low = _mm256_mul_epu32(c, simd_prime); + let high = _mm256_mul_epu32(c_high, simd_prime); + let high = _mm256_slli_epi64::<32>(high); + let result = _mm256_add_epi64(low, high); + _mm256_storeu_si256((accumulators_ptr as *mut __m256i).add(i), result); + } + } +} + +#[cfg(target_arch = "aarch64")] +unsafe fn scramble_accumulators_neon( + accumulators: &mut [u64; INIT_ACCUMULATORS.len()], + secret: &[u8], +) { + #[cfg(target_arch = "aarch64")] + use std::arch::aarch64::*; + #[cfg(target_arch = "arm")] + use std::arch::arm::*; + + unsafe { + let prime = vdup_n_u32(PRIME32[0].try_into().unwrap()); + + let accum_ptr = accumulators.as_mut_ptr(); + let secret_ptr = secret.as_ptr(); + assert!(secret.len() >= STRIPE_LENGTH); + for i in 0..(STRIPE_LENGTH / 16) { + // xorshift + let accum = vld1q_u64(accum_ptr.add(i * 2)); + let shifted = vshrq_n_u64(accum, 47); + let accum = veorq_u64(accum, shifted); + + // xor with secret + let s = vld1q_u8(secret_ptr.add(i * 16)); + let accum = veorq_u64(accum, vreinterpretq_u64_u8(s)); + + // mul with prime. Sadly there's no vmulq_u64 + let accum_low = vmovn_u64(accum); + let accum_high = vshrn_n_u64(accum, 32); + let prod_high = vshlq_n_u64(vmull_u32(accum_high, prime), 32); + let accum = vmlal_u32(prod_high, accum_low, prime); + vst1q_u64(accum_ptr.add(i * 2), accum); + } + } +} + +#[cfg(not(target_arch = "aarch64"))] +fn scramble_accumulators_generic(accumulators: &mut [u64; INIT_ACCUMULATORS.len()], secret: &[u8]) { + for (i, x) in accumulators.iter_mut().enumerate() { + let s = get_u64(secret, i); + *x = xorshift(*x, 47); + *x ^= s; + *x = x.wrapping_mul(PRIME32[0]); + } +} + +fn xorshift(x: u64, shift: u64) -> u64 { + x ^ (x >> shift) +} + +fn rrmxmx(mut x: u64, y: u64) -> u64 { + x ^= x.rotate_left(49) ^ x.rotate_left(24); + x = x.wrapping_mul(0x9FB21C651E98DF25); + x ^= (x >> 35).wrapping_add(y); + x = x.wrapping_mul(0x9FB21C651E98DF25); + xorshift(x, 28) +} + +fn mul128_and_xor(x: u64, y: u64) -> u64 { + let z = u128::from(x) * u128::from(y); + #[allow(clippy::cast_possible_truncation)] + (z as u64 ^ (z >> 64) as u64) +} + +fn mix16(data: &[u8], secret: &[u8], seed: u64) -> u64 { + let x1 = get_u64(data, 0); + let x2 = get_u64(data, 1); + let s1 = get_u64(secret, 0).wrapping_add(seed); + let s2 = get_u64(secret, 1).wrapping_sub(seed); + + mul128_and_xor(x1 ^ s1, x2 ^ s2) +} + +fn mix32(state: (u64, u64), data1: &[u8], data2: &[u8], secret: &[u8], seed: u64) -> (u64, u64) { + let (mut r_low, mut r_high) = state; + + r_low = r_low.wrapping_add(mix16(data1, secret, seed)); + r_low ^= get_u64(data2, 0).wrapping_add(get_u64(data2, 1)); + r_high = r_high.wrapping_add(mix16(data2, &secret[16..], seed)); + r_high ^= get_u64(data1, 0).wrapping_add(get_u64(data1, 1)); + + (r_low, r_high) +} + +fn gen_secret_generic(seed: u64) -> [u8; DEFAULT_SECRET.len()] { + let mut secret = [0u8; DEFAULT_SECRET.len()]; + let iterations = DEFAULT_SECRET.len() / 16; + for i in 0..iterations { + let x = get_u64(&DEFAULT_SECRET, 2 * i).wrapping_add(seed); + secret[16 * i..16 * i + 8].copy_from_slice(&x.to_le_bytes()); + let x = get_u64(&DEFAULT_SECRET, 2 * i + 1).wrapping_sub(seed); + secret[16 * i + 8..16 * (i + 1)].copy_from_slice(&x.to_le_bytes()); + } + secret +} + +#[allow(clippy::cast_possible_truncation)] +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +#[target_feature(enable = "avx2")] +unsafe fn gen_secret_avx2(seed: u64) -> [u8; DEFAULT_SECRET.len()] { + unsafe { + #[cfg(target_arch = "x86")] + use std::arch::x86::*; + #[cfg(target_arch = "x86_64")] + use std::arch::x86_64::*; + + #[allow(clippy::cast_possible_wrap)] + let xxh_i64 = 0u64.wrapping_sub(seed) as i64; + #[allow(clippy::cast_possible_wrap)] + let seed = seed as i64; + + let simd_seed = _mm256_set_epi64x(xxh_i64, seed, xxh_i64, seed); + + let mut output = [0u8; DEFAULT_SECRET.len()]; + let output_ptr = output.as_mut_ptr(); + let secret_ptr = DEFAULT_SECRET.as_ptr(); + for i in 0..6 { + let s = _mm256_loadu_si256((secret_ptr as *const __m256i).add(i)); + let x = _mm256_add_epi64(s, simd_seed); + _mm256_storeu_si256((output_ptr as *mut __m256i).add(i), x); + } + + output + } +} + +#[cfg(target_arch = "aarch64")] +unsafe fn accumulate_stripe_neon(accumulators: &mut [u64; 8], data: &[u8], secret: &[u8]) { + #[cfg(target_arch = "aarch64")] + use std::arch::aarch64::*; + #[cfg(target_arch = "arm")] + use std::arch::arm::*; + + unsafe { + let accum_ptr = accumulators.as_mut_ptr(); + let data_ptr = data.as_ptr(); + let secret_ptr = secret.as_ptr(); + assert!(data.len() >= STRIPE_LENGTH); + assert!(secret.len() >= STRIPE_LENGTH); + for i in 0..(STRIPE_LENGTH / 16) { + let x = vld1q_u8(data_ptr.add(i * 16)); + let s = vld1q_u8(secret_ptr.add(i * 16)); + let x64 = vreinterpretq_u64_u8(x); + let y = vextq_u64(x64, x64, 1); + + let result = vld1q_u64(accum_ptr.add(i * 2)); + let result = vaddq_u64(result, y); + + let z = vreinterpretq_u64_u8(veorq_u8(x, s)); + let z_low = vmovn_u64(z); + let z_high = vshrn_n_u64(z, 32); + + let result = vmlal_u32(result, z_low, z_high); + vst1q_u64(accum_ptr.add(i * 2), result); + } + } +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +#[target_feature(enable = "avx2")] +unsafe fn accumulate_stripe_avx2(accumulators: &mut [u64; 8], data: &[u8], secret: &[u8]) { + unsafe { + #[cfg(target_arch = "x86")] + use std::arch::x86::*; + #[cfg(target_arch = "x86_64")] + use std::arch::x86_64::*; + + let data_ptr = data.as_ptr(); + let secret_ptr = secret.as_ptr(); + let accumulator_ptr = accumulators.as_mut_ptr(); + + assert!(data.len() >= STRIPE_LENGTH); + assert!(secret.len() >= STRIPE_LENGTH); + for i in 0..(STRIPE_LENGTH / 32) { + let x = _mm256_loadu_si256((data_ptr as *const __m256i).add(i)); + let s = _mm256_loadu_si256((secret_ptr as *const __m256i).add(i)); + + let z = _mm256_xor_si256(x, s); + let z_low = _mm256_shuffle_epi32::<49>(z); + + let product = _mm256_mul_epu32(z, z_low); + let shuffled = _mm256_shuffle_epi32::<78>(x); + + let result = _mm256_loadu_si256((accumulator_ptr as *const __m256i).add(i)); + let result = _mm256_add_epi64(result, shuffled); + let result = _mm256_add_epi64(result, product); + _mm256_storeu_si256((accumulator_ptr as *mut __m256i).add(i), result); + } + } +} + +#[cfg(not(target_arch = "aarch64"))] +fn accumulate_stripe_generic(accumulators: &mut [u64; 8], data: &[u8], secret: &[u8]) { + for i in 0..accumulators.len() { + let x = get_u64(&data[i * 8..], 0); + let y = x ^ get_u64(&secret[i * 8..], 0); + accumulators[i ^ 1] = accumulators[i ^ 1].wrapping_add(x); + let z = (y & 0xFFFF_FFFF) * (y >> 32); + accumulators[i] = accumulators[i].wrapping_add(z) + } +} + +#[inline(always)] +fn accumulate_block( + accumulators: &mut [u64; 8], + data: &[u8], + secret: &[u8], + stripes: usize, + accum_stripe: unsafe fn(&mut [u64; 8], &[u8], &[u8]), +) { + for i in 0..stripes { + unsafe { + accum_stripe( + accumulators, + &data[i * STRIPE_LENGTH..], + &secret[i * SECRET_CONSUME_RATE..], + ); + } + } +} + +#[inline(always)] +fn hash_large_helper( + data: &[u8], + secret: &[u8], + scramble: unsafe fn(&mut [u64; 8], &[u8]), + accum_stripe: unsafe fn(&mut [u64; 8], &[u8], &[u8]), +) -> [u64; INIT_ACCUMULATORS.len()] { + let mut accumulators = INIT_ACCUMULATORS; + + let stripes_per_block = (secret.len() - STRIPE_LENGTH) / SECRET_CONSUME_RATE; + let block_len = STRIPE_LENGTH * stripes_per_block; + let blocks = (data.len() - 1) / block_len; + + // accumulate all the blocks + for i in 0..blocks { + accumulate_block( + &mut accumulators, + &data[i * block_len..], + secret, + stripes_per_block, + accum_stripe, + ); + unsafe { scramble(&mut accumulators, &secret[secret.len() - STRIPE_LENGTH..]) }; + } + + // trailing partial block + let stripes = ((data.len() - 1) - block_len * blocks) / STRIPE_LENGTH; + accumulate_block( + &mut accumulators, + &data[blocks * block_len..], + secret, + stripes, + accum_stripe, + ); + + // trailing stripe + unsafe { + accum_stripe( + &mut accumulators, + &data[data.len() - STRIPE_LENGTH..], + &secret[secret.len() - STRIPE_LENGTH - 7..], + ); + } + + accumulators +} + +fn hash64_0(secret: &[u8], seed: u64) -> u64 { + let mut result = seed; + result ^= get_u64(secret, 7); + result ^= get_u64(secret, 8); + xxh64_avalanche(result) +} + +fn hash64_1to3(data: &[u8], secret: &[u8], seed: u64) -> u64 { + let x1 = data[0] as u32; + let x2 = data[data.len() >> 1] as u32; + let x3 = (*data.last().unwrap()) as u32; + #[allow(clippy::cast_possible_truncation)] + let x4 = data.len() as u32; + + let combined = ((x1 << 16) | (x2 << 24) | x3 | (x4 << 8)) as u64; + let mut result = (get_u32(secret, 0) ^ get_u32(secret, 1)) as u64; + result = result.wrapping_add(seed); + result ^= combined; + xxh64_avalanche(result) +} + +fn hash64_4to8(data: &[u8], secret: &[u8], mut seed: u64) -> u64 { + #[allow(clippy::cast_possible_truncation)] + let truncate_seed = seed as u32; + seed ^= u64::from(truncate_seed.swap_bytes()) << 32; + let x1 = get_u32(data, 0) as u64; + let x2 = get_u32(&data[data.len() - 4..], 0) as u64; + let x = x2 | (x1 << 32); + let s = (get_u64(secret, 1) ^ get_u64(secret, 2)).wrapping_sub(seed); + rrmxmx(x ^ s, data.len() as u64) +} + +fn hash64_9to16(data: &[u8], secret: &[u8], seed: u64) -> u64 { + let s1 = (get_u64(secret, 3) ^ get_u64(secret, 4)).wrapping_add(seed); + let s2 = (get_u64(secret, 5) ^ get_u64(secret, 6)).wrapping_sub(seed); + let x1 = get_u64(data, 0) ^ s1; + let x2 = get_u64(&data[data.len() - 8..], 0) ^ s2; + let mut result = data.len() as u64; + result = result.wrapping_add(x1.swap_bytes()); + result = result.wrapping_add(x2); + result = result.wrapping_add(mul128_and_xor(x1, x2)); + xxh3_avalanche(result) +} + +fn hash64_0to16(data: &[u8], secret: &[u8], seed: u64) -> u64 { + if data.is_empty() { + hash64_0(secret, seed) + } else if data.len() < 4 { + hash64_1to3(data, secret, seed) + } else if data.len() <= 8 { + hash64_4to8(data, secret, seed) + } else { + hash64_9to16(data, secret, seed) + } +} + +fn hash64_17to128(data: &[u8], secret: &[u8], seed: u64) -> u64 { + let mut result = PRIME64[0].wrapping_mul(data.len() as u64); + let iterations = (data.len() - 1) / 32; + for i in (0..=iterations).rev() { + result = result.wrapping_add(mix16(&data[16 * i..], &secret[32 * i..], seed)); + result = result.wrapping_add(mix16( + &data[data.len() - 16 * (i + 1)..], + &secret[32 * i + 16..], + seed, + )); + } + xxh3_avalanche(result) +} + +fn hash64_129to240(data: &[u8], secret: &[u8], seed: u64) -> u64 { + let mut result = PRIME64[0].wrapping_mul(data.len() as u64); + for i in 0..8 { + result = result.wrapping_add(mix16(&data[16 * i..], &secret[16 * i..], seed)); + } + result = xxh3_avalanche(result); + let iterations = data.len() / 16; + for i in 8..iterations { + result = result.wrapping_add(mix16(&data[16 * i..], &secret[16 * (i - 8) + 3..], seed)); + } + result = result.wrapping_add(mix16( + &data[data.len() - 16..], + &secret[MIN_SECRET_SIZE - 17..], + seed, + )); + + xxh3_avalanche(result) +} + +fn hash64_0to240(data: &[u8], secret: &[u8], seed: u64) -> u64 { + if data.len() <= 16 { + hash64_0to16(data, secret, seed) + } else if data.len() <= 128 { + hash64_17to128(data, secret, seed) + } else { + hash64_129to240(data, secret, seed) + } +} + +#[cfg(target_arch = "aarch64")] +unsafe fn hash64_large_neon(data: &[u8], seed: u64) -> u64 { + hash64_large_generic( + data, + seed, + gen_secret_generic, + scramble_accumulators_neon, + accumulate_stripe_neon, + ) +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +#[target_feature(enable = "avx2")] +unsafe fn hash64_large_avx2(data: &[u8], seed: u64) -> u64 { + hash64_large_generic( + data, + seed, + gen_secret_avx2, + scramble_accumulators_avx2, + accumulate_stripe_avx2, + ) +} + +#[inline(always)] +fn hash64_large_generic( + data: &[u8], + seed: u64, + generate: unsafe fn(u64) -> [u8; DEFAULT_SECRET.len()], + scramble: unsafe fn(&mut [u64; 8], &[u8]), + accum_stripe: unsafe fn(&mut [u64; 8], &[u8], &[u8]), +) -> u64 { + let secret = unsafe { generate(seed) }; + let accumulators = hash_large_helper(data, &secret, scramble, accum_stripe); + + merge_accumulators( + accumulators, + &secret[11..], + PRIME64[0].wrapping_mul(data.len() as u64), + ) +} + +fn hash128_0(secret: &[u8], seed: u64) -> u128 { + let high = (hash64_0(&secret[3 * 8..], seed) as u128) << 64; + let low = hash64_0(&secret[8..], seed) as u128; + high | low +} + +fn hash128_1to3(data: &[u8], secret: &[u8], seed: u64) -> u128 { + let x1 = data[0] as u32; + let x2 = data[data.len() >> 1] as u32; + let x3 = (*data.last().unwrap()) as u32; + #[allow(clippy::cast_possible_truncation)] + let x4 = data.len() as u32; + + let combined_low = (x1 << 16) | (x2 << 24) | x3 | (x4 << 8); + let combined_high: u64 = combined_low.swap_bytes().rotate_left(13).into(); + let s_low = ((get_u32(secret, 0) ^ get_u32(secret, 1)) as u64).wrapping_add(seed); + let s_high = ((get_u32(secret, 2) ^ get_u32(secret, 3)) as u64).wrapping_sub(seed); + let high = (xxh64_avalanche(combined_high ^ s_high) as u128) << 64; + let low = xxh64_avalanche(combined_low as u64 ^ s_low) as u128; + high | low +} + +fn hash128_4to8(data: &[u8], secret: &[u8], mut seed: u64) -> u128 { + #[allow(clippy::cast_possible_truncation)] + let truncate_seed = seed as u32; + seed ^= u64::from(truncate_seed.swap_bytes()) << 32; + let x_low = get_u32(data, 0) as u64; + let x_high = u32::from_le_bytes(data[data.len() - 4..].try_into().unwrap()) as u64; + let x = x_low | (x_high << 32); + let s = (get_u64(secret, 2) ^ get_u64(secret, 3)).wrapping_add(seed); + + let mut y = (x ^ s) as u128; + y = y.wrapping_mul(PRIME64[0].wrapping_add((data.len() << 2) as u64) as u128); + + #[allow(clippy::cast_possible_truncation)] + let mut r_low = y as u64; + let mut r_high: u64 = (y >> 64).try_into().unwrap(); + r_high = r_high.wrapping_add(r_low << 1); + r_low ^= r_high >> 3; + r_low = xorshift(r_low, 35); + r_low = r_low.wrapping_mul(0x9FB21C651E98DF25); + r_low = xorshift(r_low, 28); + r_high = xxh3_avalanche(r_high); + + ((r_high as u128) << 64) | r_low as u128 +} + +fn hash128_9to16(data: &[u8], secret: &[u8], seed: u64) -> u128 { + let s_low = (get_u64(secret, 4) ^ get_u64(secret, 5)).wrapping_sub(seed); + let s_high = (get_u64(secret, 6) ^ get_u64(secret, 7)).wrapping_add(seed); + let x_low = get_u64(data, 0); + let x_high = u64::from_le_bytes(data[data.len() - 8..].try_into().unwrap()); + let mixed = x_low ^ x_high ^ s_low; + let x_high = x_high ^ s_high; + + let result = (mixed as u128).wrapping_mul(PRIME64[0] as u128); + #[allow(clippy::cast_possible_truncation)] + let mut r_low = result as u64; + let mut r_high = (result >> 64) as u64; + r_low = r_low.wrapping_add((data.len() as u64 - 1) << 54); + r_high = r_high.wrapping_add(x_high); + r_high = r_high.wrapping_add((x_high & 0xFFFF_FFFF).wrapping_mul(PRIME32[1] - 1)); + r_low ^= r_high.swap_bytes(); + + let result2 = (r_low as u128).wrapping_mul(PRIME64[1] as u128); + #[allow(clippy::cast_possible_truncation)] + let mut r2_low = result2 as u64; + let mut r2_high = (result2 >> 64) as u64; + r2_high = r2_high.wrapping_add(r_high.wrapping_mul(PRIME64[1])); + r2_low = xxh3_avalanche(r2_low); + r2_high = xxh3_avalanche(r2_high); + + ((r2_high as u128) << 64) | r2_low as u128 +} + +fn hash128_0to16(data: &[u8], secret: &[u8], seed: u64) -> u128 { + if data.is_empty() { + hash128_0(secret, seed) + } else if data.len() < 4 { + hash128_1to3(data, secret, seed) + } else if data.len() <= 8 { + hash128_4to8(data, secret, seed) + } else { + hash128_9to16(data, secret, seed) + } +} + +fn hash128_17to128(data: &[u8], secret: &[u8], seed: u64) -> u128 { + let len = data.len(); + let mut state = (PRIME64[0].wrapping_mul(len as u64), 0); + if len > 32 { + if len > 64 { + if len > 96 { + state = mix32(state, &data[48..], &data[len - 64..], &secret[96..], seed); + } + state = mix32(state, &data[32..], &data[len - 48..], &secret[64..], seed); + } + state = mix32(state, &data[16..], &data[len - 32..], &secret[32..], seed); + } + state = mix32(state, data, &data[len - 16..], secret, seed); + + let mut r_low = state.0.wrapping_add(state.1); + let mut r_high = state.0.wrapping_mul(PRIME64[0]); + r_high = r_high.wrapping_add(state.1.wrapping_mul(PRIME64[3])); + r_high = r_high.wrapping_add((len as u64).wrapping_sub(seed).wrapping_mul(PRIME64[1])); + r_low = xxh3_avalanche(r_low); + r_high = 0u64.wrapping_sub(xxh3_avalanche(r_high)); + + ((r_high as u128) << 64) | r_low as u128 +} + +fn hash128_129to240(data: &[u8], secret: &[u8], seed: u64) -> u128 { + let len = data.len(); + let iterations = len / 32; + let mut state = (PRIME64[0].wrapping_mul(len as u64), 0); + + for i in 0..4 { + state = mix32( + state, + &data[32 * i..], + &data[32 * i + 16..], + &secret[32 * i..], + seed, + ); + } + state.0 = xxh3_avalanche(state.0); + state.1 = xxh3_avalanche(state.1); + + for i in 4..iterations { + state = mix32( + state, + &data[32 * i..], + &data[32 * i + 16..], + &secret[3 + 32 * (i - 4)..], + seed, + ); + } + state = mix32( + state, + &data[len - 16..], + &data[len - 32..], + &secret[MIN_SECRET_SIZE - 33..], + 0u64.wrapping_sub(seed), + ); + + let mut r_low = state.0.wrapping_add(state.1); + let mut r_high = state.0.wrapping_mul(PRIME64[0]); + r_high = r_high.wrapping_add(state.1.wrapping_mul(PRIME64[3])); + r_high = r_high.wrapping_add((len as u64).wrapping_sub(seed).wrapping_mul(PRIME64[1])); + r_low = xxh3_avalanche(r_low); + r_high = 0u64.wrapping_sub(xxh3_avalanche(r_high)); + + ((r_high as u128) << 64) | r_low as u128 +} + +fn hash128_0to240(data: &[u8], secret: &[u8], seed: u64) -> u128 { + if data.len() <= 16 { + hash128_0to16(data, secret, seed) + } else if data.len() <= 128 { + hash128_17to128(data, secret, seed) + } else { + hash128_129to240(data, secret, seed) + } +} + +#[cfg(target_arch = "aarch64")] +unsafe fn hash128_large_neon(data: &[u8], seed: u64) -> u128 { + hash128_large_generic( + data, + seed, + gen_secret_generic, + scramble_accumulators_neon, + accumulate_stripe_neon, + ) +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +#[target_feature(enable = "avx2")] +unsafe fn hash128_large_avx2(data: &[u8], seed: u64) -> u128 { + hash128_large_generic( + data, + seed, + gen_secret_avx2, + scramble_accumulators_avx2, + accumulate_stripe_avx2, + ) +} + +#[inline(always)] +fn hash128_large_generic( + data: &[u8], + seed: u64, + generate: unsafe fn(u64) -> [u8; DEFAULT_SECRET.len()], + scramble: unsafe fn(&mut [u64; 8], &[u8]), + accum_stripe: unsafe fn(&mut [u64; 8], &[u8], &[u8]), +) -> u128 { + let secret = unsafe { generate(seed) }; + let accumulators = hash_large_helper(data, &secret, scramble, accum_stripe); + + let low = merge_accumulators( + accumulators, + &secret[11..], + PRIME64[0].wrapping_mul(data.len() as u64), + ); + let high = merge_accumulators( + accumulators, + &secret[secret.len() - 64 - 11..], + !(PRIME64[1].wrapping_mul(data.len() as u64)), + ); + + ((high as u128) << 64) | low as u128 +} + +#[cfg(test)] +mod test { + use crate::tree_store::page_store::xxh3::hash64_with_seed; + + #[test] + fn test_empty() { + let actual = hash64_with_seed(&[], 0); + assert_eq!(actual, 3244421341483603138); + } +} diff --git a/v2/redb/src/tree_store/table_tree.rs b/v2/redb/src/tree_store/table_tree.rs new file mode 100644 index 000000000..09d194e58 --- /dev/null +++ b/v2/redb/src/tree_store/table_tree.rs @@ -0,0 +1,785 @@ +use crate::db::TransactionGuard; +use crate::error::TableError; +use crate::multimap_table::{ + finalize_tree_and_subtree_checksums, multimap_btree_stats, verify_tree_and_subtree_checksums, +}; +use crate::tree_store::btree::{UntypedBtreeMut, btree_stats}; +use crate::tree_store::btree_base::BtreeHeader; +use crate::tree_store::{ + Btree, BtreeMut, BtreeRangeIter, InternalTableDefinition, PageHint, PageNumber, PagePath, + PageTrackerPolicy, RawBtree, TableType, TransactionalMemory, +}; +use crate::types::{Key, Value}; +use crate::{DatabaseStats, Result}; +use std::cmp::max; +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::mem::size_of; +use std::ops::RangeFull; +use std::sync::{Arc, Mutex}; +use std::{mem, thread}; + +#[derive(Debug)] +#[repr(transparent)] +pub(crate) struct PageListMut { + data: [u8], +} + +impl PageListMut { + pub(crate) fn push_back(&mut self, value: PageNumber) { + let len = u16::from_le_bytes(self.data[..size_of::()].try_into().unwrap()); + self.data[..size_of::()].copy_from_slice(&(len + 1).to_le_bytes()); + let len: usize = len.into(); + let start = size_of::() + PageNumber::serialized_size() * len; + self.data[start..(start + PageNumber::serialized_size())] + .copy_from_slice(&value.to_le_bytes()); + } + + pub(crate) fn clear(&mut self) { + self.data[..size_of::()].fill(0); + } +} + +pub struct TableNameIter { + inner: BtreeRangeIter<&'static str, InternalTableDefinition>, + table_type: TableType, +} + +impl Iterator for TableNameIter { + type Item = Result; + + fn next(&mut self) -> Option { + for entry in self.inner.by_ref() { + match entry { + Ok(entry) => { + if entry.value().get_type() == self.table_type { + return Some(Ok(entry.key().to_string())); + } + } + Err(err) => { + return Some(Err(err)); + } + } + } + None + } +} + +pub(crate) struct TableTree { + tree: Btree<&'static str, InternalTableDefinition>, + mem: Arc, +} + +impl TableTree { + pub(crate) fn new( + master_root: Option, + page_hint: PageHint, + guard: Arc, + mem: Arc, + ) -> Result { + Ok(Self { + tree: Btree::new(master_root, page_hint, guard, mem.clone())?, + mem, + }) + } + + pub(crate) fn transaction_guard(&self) -> &Arc { + self.tree.transaction_guard() + } + + pub(crate) fn verify_checksums(&self) -> Result { + if !self.tree.verify_checksum()? { + return Ok(false); + } + + for entry in self.tree.range::(&(..))? { + let entry = entry?; + let definition = entry.value(); + match definition { + InternalTableDefinition::Normal { + table_root, + fixed_key_size, + fixed_value_size, + .. + } => { + if let Some(header) = table_root + && !RawBtree::new( + Some(header), + fixed_key_size, + fixed_value_size, + self.mem.clone(), + self.tree.hint(), + ) + .verify_checksum()? + { + return Ok(false); + } + } + InternalTableDefinition::Multimap { + table_root, + fixed_key_size, + fixed_value_size, + .. + } => { + if !verify_tree_and_subtree_checksums( + table_root, + fixed_key_size, + fixed_value_size, + self.mem.clone(), + self.tree.hint(), + )? { + return Ok(false); + } + } + } + } + + Ok(true) + } + + // root_page: the root of the master table + pub(crate) fn list_tables(&self, table_type: TableType) -> Result> { + let iter = self.tree.range::(&(..))?; + let iter = TableNameIter { + inner: iter, + table_type, + }; + let mut result = vec![]; + for table in iter { + result.push(table?); + } + Ok(result) + } + + pub(crate) fn get_table_untyped( + &self, + name: &str, + table_type: TableType, + ) -> Result, TableError> { + if let Some(guard) = self.tree.get(&name)? { + let definition = guard.value(); + definition.check_match_untyped(table_type, name)?; + Ok(Some(definition)) + } else { + Ok(None) + } + } + + // root_page: the root of the master table + pub(crate) fn get_table( + &self, + name: &str, + table_type: TableType, + ) -> Result, TableError> { + Ok( + if let Some(definition) = self.get_table_untyped(name, table_type)? { + // Do additional checks on the types to be sure they match + definition.check_match::(table_type, name)?; + Some(definition) + } else { + None + }, + ) + } + + pub(crate) fn visit_all_pages(&self, mut visitor: F) -> Result + where + F: FnMut(&PagePath) -> Result, + { + // All the pages in the table tree itself + self.tree.visit_all_pages(&mut visitor)?; + + // All the normal tables + for entry in self.list_tables(TableType::Normal)? { + let definition = self + .get_table_untyped(&entry, TableType::Normal) + .map_err(|e| e.into_storage_error_or_corrupted("Internal corruption"))? + .unwrap(); + definition.visit_all_pages(self.mem.clone(), self.tree.hint(), |path| visitor(path))?; + } + + for entry in self.list_tables(TableType::Multimap)? { + let definition = self + .get_table_untyped(&entry, TableType::Multimap) + .map_err(|e| e.into_storage_error_or_corrupted("Internal corruption"))? + .unwrap(); + definition.visit_all_pages(self.mem.clone(), self.tree.hint(), |path| visitor(path))?; + } + + Ok(()) + } +} + +pub(crate) struct TableTreeMut<'txn> { + tree: BtreeMut<'txn, &'static str, InternalTableDefinition>, + guard: Arc, + mem: Arc, + // Cached updates from tables that have been closed. These must be flushed to the btree. + // The bool indicates whether the root has dirty (DEFERRED) checksums that need finalization. + pending_table_updates: HashMap, u64, bool)>, + freed_pages: Arc>>, + allocated_pages: Arc>, +} + +impl TableTreeMut<'_> { + pub(crate) fn new( + master_root: Option, + guard: Arc, + mem: Arc, + freed_pages: Arc>>, + allocated_pages: Arc>, + ) -> Self { + Self { + tree: BtreeMut::new( + master_root, + guard.clone(), + mem.clone(), + freed_pages.clone(), + allocated_pages.clone(), + ), + guard, + mem, + pending_table_updates: Default::default(), + freed_pages, + allocated_pages, + } + } + + pub(crate) fn set_root(&mut self, root: Option) { + self.tree.set_root(root); + // Pending updates were staged for the old root and are invalid for the new one + self.pending_table_updates.clear(); + } + + #[cfg_attr(not(debug_assertions), expect(dead_code))] + pub(crate) fn visit_all_pages(&self, mut visitor: F) -> Result + where + F: FnMut(&PagePath) -> Result, + { + // All the pages in the table tree itself + self.tree.visit_all_pages(&mut visitor)?; + + // All the normal tables + for entry in self.list_tables(TableType::Normal)? { + let definition = self + .get_table_untyped(&entry, TableType::Normal) + .map_err(|e| e.into_storage_error_or_corrupted("Internal corruption"))? + .unwrap(); + definition.visit_all_pages(self.mem.clone(), PageHint::None, |path| visitor(path))?; + } + + for entry in self.list_tables(TableType::Multimap)? { + let definition = self + .get_table_untyped(&entry, TableType::Multimap) + .map_err(|e| e.into_storage_error_or_corrupted("Internal corruption"))? + .unwrap(); + definition.visit_all_pages(self.mem.clone(), PageHint::None, |path| visitor(path))?; + } + + Ok(()) + } + + // Queues an update to the table root + pub(crate) fn stage_update_table_root( + &mut self, + name: &str, + table_root: Option, + length: u64, + ) { + let dirty = table_root + .as_ref() + .is_some_and(|header| self.mem.uncommitted(header.root)); + self.pending_table_updates + .insert(name.to_string(), (table_root, length, dirty)); + } + + pub(crate) fn clear_root_updates_and_close(&mut self) { + self.pending_table_updates.clear(); + self.allocated_pages.lock().unwrap().close(); + } + + pub(crate) fn flush_and_close( + &mut self, + ) -> Result<(Option, HashSet, Vec)> { + match self.flush_inner() { + Ok(header) => { + let allocated = self.allocated_pages.lock()?.close(); + let mut old = vec![]; + let mut freed_pages = self.freed_pages.lock()?; + mem::swap(freed_pages.as_mut(), &mut old); + Ok((header, allocated, old)) + } + Err(err) => { + // Ensure that the allocated pages get clear. Otherwise it will cause a panic + // when they are dropped + self.allocated_pages.lock()?.close(); + Err(err) + } + } + } + + fn flush_inner(&mut self) -> Result> { + self.flush_table_root_updates()?.finalize_dirty_checksums() + } + + pub(crate) fn flush_table_root_updates(&mut self) -> Result<&mut Self> { + for (name, (new_root, new_length, dirty)) in self.pending_table_updates.drain() { + // Bypass .get_table() since the table types are dynamic + let mut definition = self.tree.get(&name.as_str())?.unwrap().value(); + // No-op if the root has not changed and checksums are already finalized + if !dirty { + match definition { + InternalTableDefinition::Normal { table_root, .. } + | InternalTableDefinition::Multimap { table_root, .. } => { + if table_root == new_root { + continue; + } + } + } + } + // Finalize any dirty checksums + match definition { + InternalTableDefinition::Normal { + ref mut table_root, + ref mut table_length, + fixed_key_size, + fixed_value_size, + .. + } => { + let mut tree = UntypedBtreeMut::new( + new_root, + self.mem.clone(), + self.freed_pages.clone(), + fixed_key_size, + fixed_value_size, + ); + *table_root = tree.finalize_dirty_checksums()?; + *table_length = new_length; + } + InternalTableDefinition::Multimap { + ref mut table_root, + ref mut table_length, + fixed_key_size, + fixed_value_size, + .. + } => { + *table_root = finalize_tree_and_subtree_checksums( + new_root, + fixed_key_size, + fixed_value_size, + self.mem.clone(), + )?; + *table_length = new_length; + } + } + self.tree.insert(&name.as_str(), &definition)?; + } + Ok(self) + } + + // Opens a table, calls the provided closure to insert entries into it, and then + // flushes the table root. The flush is done using insert_inplace(), so it's guaranteed + // that no pages will be allocated or freed after the closure returns + pub(crate) fn open_table_and_flush_table_root( + &mut self, + name: &str, + f: impl FnOnce(&mut BtreeMut) -> Result, + ) -> Result { + assert!(self.pending_table_updates.is_empty()); + + // Reserve space in the table tree + // TODO: maybe we should have a more explicit method, like "force_uncommitted()" + let existing = self + .tree + .insert( + &name, + &InternalTableDefinition::new::(TableType::Normal, None, 0), + )? + .map(|x| x.value()); + if let Some(existing) = existing { + self.tree.insert(&name, &existing)?; + } + + let table_root = match self.tree.get(&name)?.unwrap().value() { + InternalTableDefinition::Normal { table_root, .. } => table_root, + InternalTableDefinition::Multimap { .. } => { + unreachable!() + } + }; + + // Open the table and call the provided closure on it + let mut tree: BtreeMut = BtreeMut::new( + table_root, + self.guard.clone(), + self.mem.clone(), + self.freed_pages.clone(), + self.allocated_pages.clone(), + ); + f(&mut tree)?; + + // Finalize the table's checksums + let table_root = tree.finalize_dirty_checksums()?; + let table_length = tree.get_root().map(|x| x.length).unwrap_or_default(); + + // Flush the root to the table tree, without allocating + self.tree.insert_inplace( + &name, + &InternalTableDefinition::new::(TableType::Normal, table_root, table_length), + )?; + + Ok(()) + } + + // Creates a new table, calls the provided closure to insert entries into it, and then + // flushes the table root. The flush is done using insert_inplace(), so it's guaranteed + // that no pages will be allocated or freed after the closure returns + pub(crate) fn create_table_and_flush_table_root( + &mut self, + name: &str, + f: impl FnOnce(&mut Self, &mut BtreeMut) -> Result, + ) -> Result { + assert!(self.pending_table_updates.is_empty()); + assert!(self.tree.get(&name)?.is_none()); + + // Reserve space in the table tree + self.tree.insert( + &name, + &InternalTableDefinition::new::(TableType::Normal, None, 0), + )?; + + // Create an empty table and call the provided closure on it + let mut tree: BtreeMut = BtreeMut::new( + None, + self.guard.clone(), + self.mem.clone(), + self.freed_pages.clone(), + self.allocated_pages.clone(), + ); + f(self, &mut tree)?; + + // Finalize the table's checksums + let table_root = tree.finalize_dirty_checksums()?; + let table_length = tree.get_root().map(|x| x.length).unwrap_or_default(); + + // Flush the root to the table tree, without allocating + self.tree.insert_inplace( + &name, + &InternalTableDefinition::new::(TableType::Normal, table_root, table_length), + )?; + + Ok(()) + } + + pub(crate) fn finalize_dirty_checksums(&mut self) -> Result> { + self.tree.finalize_dirty_checksums() + } + + // root_page: the root of the master table + pub(crate) fn list_tables(&self, table_type: TableType) -> Result> { + let tree = TableTree::new( + self.tree.get_root(), + PageHint::None, + self.guard.clone(), + self.mem.clone(), + )?; + tree.list_tables(table_type) + } + + pub(crate) fn get_table_untyped( + &self, + name: &str, + table_type: TableType, + ) -> Result, TableError> { + let tree = TableTree::new( + self.tree.get_root(), + PageHint::None, + self.guard.clone(), + self.mem.clone(), + )?; + let mut result = tree.get_table_untyped(name, table_type); + + if let Ok(Some(definition)) = result.as_mut() + && let Some((updated_root, updated_length, _)) = self.pending_table_updates.get(name) + { + definition.set_header(*updated_root, *updated_length); + } + + result + } + + // root_page: the root of the master table + pub(crate) fn get_table( + &self, + name: &str, + table_type: TableType, + ) -> Result, TableError> { + let tree = TableTree::new( + self.tree.get_root(), + PageHint::None, + self.guard.clone(), + self.mem.clone(), + )?; + let mut result = tree.get_table::(name, table_type); + + if let Ok(Some(definition)) = result.as_mut() + && let Some((updated_root, updated_length, _)) = self.pending_table_updates.get(name) + { + definition.set_header(*updated_root, *updated_length); + } + + result + } + + pub(crate) fn rename_table( + &mut self, + name: &str, + new_name: &str, + table_type: TableType, + ) -> Result<(), TableError> { + if let Some(definition) = self.get_table_untyped(name, table_type)? { + if self.get_table_untyped(new_name, table_type)?.is_some() { + return Err(TableError::TableExists(new_name.to_string())); + } + if let Some(update) = self.pending_table_updates.remove(name) { + self.pending_table_updates + .insert(new_name.to_string(), update); + } + assert!(self.tree.remove(&name)?.is_some()); + assert!(self.tree.insert(&new_name, &definition)?.is_none()); + } else { + return Err(TableError::TableDoesNotExist(name.to_string())); + } + + Ok(()) + } + + pub(crate) fn delete_table( + &mut self, + name: &str, + table_type: TableType, + ) -> Result { + if let Some(definition) = self.get_table_untyped(name, table_type)? { + // Collect all pages first, then free them. The walk reads each page to discover + // its children, so we must not invalidate any page before the walk completes. + let mut pages = vec![]; + definition.visit_all_pages(self.mem.clone(), PageHint::None, |path| { + pages.push(path.page_number()); + Ok(()) + })?; + let mut freed_pages = self.freed_pages.lock().unwrap(); + let mut allocated_pages = self.allocated_pages.lock().unwrap(); + for page in pages { + if !self.mem.free_if_uncommitted(page, &mut allocated_pages) { + freed_pages.push(page); + } + } + drop(allocated_pages); + drop(freed_pages); + + self.pending_table_updates.remove(name); + + let found = self.tree.remove(&name)?.is_some(); + return Ok(found); + } + + Ok(false) + } + + pub(crate) fn get_or_create_table( + &mut self, + name: &str, + table_type: TableType, + ) -> Result<(Option, u64), TableError> { + let table = if let Some(found) = self.get_table::(name, table_type)? { + found + } else { + let table = InternalTableDefinition::new::(table_type, None, 0); + self.tree.insert(&name, &table)?; + table + }; + + match table { + InternalTableDefinition::Normal { + table_root, + table_length, + .. + } + | InternalTableDefinition::Multimap { + table_root, + table_length, + .. + } => Ok((table_root, table_length)), + } + } + + // Returns the paths to the n pages that are closest to the end of the database + // The return value is sorted, according to path.page_number()'s Ord + pub(crate) fn highest_index_pages( + &self, + n: usize, + output: &mut BTreeMap, + ) -> Result { + for entry in self.tree.range::(&(..))? { + let entry = entry?; + let mut definition = entry.value(); + if let Some((updated_root, updated_length, _)) = + self.pending_table_updates.get(entry.key()) + { + definition.set_header(*updated_root, *updated_length); + } + + definition.visit_all_pages(self.mem.clone(), PageHint::None, |path| { + output.insert(path.page_number(), path.clone()); + while output.len() > n { + output.pop_first(); + } + Ok(()) + })?; + } + + self.tree.visit_all_pages(|path| { + output.insert(path.page_number(), path.clone()); + while output.len() > n { + output.pop_first(); + } + Ok(()) + })?; + + Ok(()) + } + + pub(crate) fn relocate_tables( + &mut self, + relocation_map: &HashMap, + ) -> Result { + for entry in self.tree.range::(&(..))? { + let entry = entry?; + let mut definition = entry.value(); + if let Some((updated_root, updated_length, _)) = + self.pending_table_updates.get(entry.key()) + { + definition.set_header(*updated_root, *updated_length); + } + + if let Some(new_root) = definition.relocate_tree( + self.mem.clone(), + self.freed_pages.clone(), + relocation_map, + )? { + // Relocated roots have already-finalized checksums + self.pending_table_updates.insert( + entry.key().to_string(), + (Some(new_root), definition.get_length(), false), + ); + } + } + + self.tree.relocate(relocation_map)?; + + Ok(()) + } + + pub fn stats(&self) -> Result { + let master_tree_stats = self.tree.stats()?; + let mut max_subtree_height = 0; + let mut total_stored_bytes = 0; + // Count the master tree leaf pages as branches, since they point to the data trees + let mut branch_pages = master_tree_stats.branch_pages + master_tree_stats.leaf_pages; + let mut leaf_pages = 0; + // Include the master table in the overhead + let mut total_metadata_bytes = + master_tree_stats.metadata_bytes + master_tree_stats.stored_leaf_bytes; + let mut total_fragmented = master_tree_stats.fragmented_bytes; + + for entry in self.tree.range::(&(..))? { + let entry = entry?; + let mut definition = entry.value(); + if let Some((updated_root, length, _)) = self.pending_table_updates.get(entry.key()) { + definition.set_header(*updated_root, *length); + } + match definition { + InternalTableDefinition::Normal { + table_root, + fixed_key_size, + fixed_value_size, + .. + } => { + let subtree_stats = btree_stats( + table_root.map(|x| x.root), + &self.mem, + fixed_key_size, + fixed_value_size, + PageHint::None, + )?; + max_subtree_height = max(max_subtree_height, subtree_stats.tree_height); + total_stored_bytes += subtree_stats.stored_leaf_bytes; + total_metadata_bytes += subtree_stats.metadata_bytes; + total_fragmented += subtree_stats.fragmented_bytes; + branch_pages += subtree_stats.branch_pages; + leaf_pages += subtree_stats.leaf_pages; + } + InternalTableDefinition::Multimap { + table_root, + fixed_key_size, + fixed_value_size, + .. + } => { + let subtree_stats = multimap_btree_stats( + table_root.map(|x| x.root), + &self.mem, + fixed_key_size, + fixed_value_size, + PageHint::None, + )?; + max_subtree_height = max(max_subtree_height, subtree_stats.tree_height); + total_stored_bytes += subtree_stats.stored_leaf_bytes; + total_metadata_bytes += subtree_stats.metadata_bytes; + total_fragmented += subtree_stats.fragmented_bytes; + branch_pages += subtree_stats.branch_pages; + leaf_pages += subtree_stats.leaf_pages; + } + } + } + Ok(DatabaseStats { + tree_height: master_tree_stats.tree_height + max_subtree_height, + allocated_pages: self.mem.count_allocated_pages()?, + leaf_pages, + branch_pages, + stored_leaf_bytes: total_stored_bytes, + metadata_bytes: total_metadata_bytes, + fragmented_bytes: total_fragmented, + page_size: self.mem.get_page_size(), + }) + } +} + +impl Drop for TableTreeMut<'_> { + fn drop(&mut self) { + if thread::panicking() { + return; + } + assert!(self.allocated_pages.lock().unwrap().is_empty()); + } +} + +#[cfg(test)] +mod test { + use crate::Value; + use crate::tree_store::table_tree_base::InternalTableDefinition; + use crate::types::TypeName; + + #[test] + fn round_trip() { + let x = InternalTableDefinition::Multimap { + table_root: None, + table_length: 0, + fixed_key_size: None, + fixed_value_size: Some(5), + key_alignment: 6, + value_alignment: 7, + key_type: TypeName::new("test::Key"), + value_type: TypeName::new("test::Value"), + }; + let y = InternalTableDefinition::from_bytes(InternalTableDefinition::as_bytes(&x).as_ref()); + assert_eq!(x, y); + } +} diff --git a/v2/redb/src/tree_store/table_tree_base.rs b/v2/redb/src/tree_store/table_tree_base.rs new file mode 100644 index 000000000..3bb48ffc4 --- /dev/null +++ b/v2/redb/src/tree_store/table_tree_base.rs @@ -0,0 +1,507 @@ +use crate::multimap_table::{UntypedMultiBtree, relocate_subtrees}; +use crate::tree_store::{ + BtreeHeader, PageHint, PageNumber, PagePath, TransactionalMemory, UntypedBtree, UntypedBtreeMut, +}; +use crate::{Key, Result, TableError, TypeName, Value}; +use std::collections::HashMap; +use std::mem::size_of; +use std::sync::{Arc, Mutex}; + +// Forward compatibility feature in case alignment can be supported in the future +// See https://github.com/cberner/redb/issues/360 +const ALIGNMENT: usize = 1; + +#[derive(Copy, Clone, Hash, Eq, PartialEq, Debug)] +pub(crate) enum TableType { + Normal, + Multimap, +} + +impl TableType { + fn is_legacy(value: u8) -> bool { + value == 1 || value == 2 + } +} + +#[allow(clippy::from_over_into)] +impl Into for TableType { + fn into(self) -> u8 { + match self { + // 1 & 2 were used in the v1 file format + // TableType::Normal => 1, + // TableType::Multimap => 2, + TableType::Normal => 3, + TableType::Multimap => 4, + } + } +} + +impl From for TableType { + fn from(value: u8) -> Self { + match value { + 3 => TableType::Normal, + 4 => TableType::Multimap, + _ => unreachable!(), + } + } +} + +#[derive(Clone, PartialEq, Debug)] +pub(crate) enum InternalTableDefinition { + Normal { + table_root: Option, + table_length: u64, + fixed_key_size: Option, + fixed_value_size: Option, + key_alignment: usize, + value_alignment: usize, + key_type: TypeName, + value_type: TypeName, + }, + Multimap { + table_root: Option, + table_length: u64, + fixed_key_size: Option, + fixed_value_size: Option, + key_alignment: usize, + value_alignment: usize, + key_type: TypeName, + value_type: TypeName, + }, +} + +impl InternalTableDefinition { + pub(super) fn new( + table_type: TableType, + table_root: Option, + table_length: u64, + ) -> Self { + match table_type { + TableType::Normal => InternalTableDefinition::Normal { + table_root, + table_length, + fixed_key_size: K::fixed_width(), + fixed_value_size: V::fixed_width(), + key_alignment: ALIGNMENT, + value_alignment: ALIGNMENT, + key_type: K::type_name(), + value_type: V::type_name(), + }, + TableType::Multimap => InternalTableDefinition::Multimap { + table_root, + table_length, + fixed_key_size: K::fixed_width(), + fixed_value_size: V::fixed_width(), + key_alignment: ALIGNMENT, + value_alignment: ALIGNMENT, + key_type: K::type_name(), + value_type: V::type_name(), + }, + } + } + + pub(super) fn set_header(&mut self, root: Option, length: u64) { + match self { + InternalTableDefinition::Normal { + table_root, + table_length, + .. + } + | InternalTableDefinition::Multimap { + table_root, + table_length, + .. + } => { + *table_root = root; + *table_length = length; + } + } + } + + pub(super) fn check_match_untyped( + &self, + table_type: TableType, + name: &str, + ) -> Result<(), TableError> { + if self.get_type() != table_type { + return if self.get_type() == TableType::Multimap { + Err(TableError::TableIsMultimap(name.to_string())) + } else { + Err(TableError::TableIsNotMultimap(name.to_string())) + }; + } + if self.private_get_key_alignment() != ALIGNMENT { + return Err(TableError::TypeDefinitionChanged { + name: self.private_key_type(), + alignment: self.private_get_key_alignment(), + width: self.private_get_fixed_key_size(), + }); + } + if self.private_get_value_alignment() != ALIGNMENT { + return Err(TableError::TypeDefinitionChanged { + name: self.private_value_type(), + alignment: self.private_get_value_alignment(), + width: self.private_get_fixed_value_size(), + }); + } + + Ok(()) + } + + pub(super) fn check_match( + &self, + table_type: TableType, + name: &str, + ) -> Result<(), TableError> { + self.check_match_untyped(table_type, name)?; + + if self.private_key_type() != K::type_name() || self.private_value_type() != V::type_name() + { + return Err(TableError::TableTypeMismatch { + table: name.to_string(), + key: self.private_key_type(), + value: self.private_value_type(), + }); + } + if self.private_get_fixed_key_size() != K::fixed_width() { + return Err(TableError::TypeDefinitionChanged { + name: K::type_name(), + alignment: self.private_get_key_alignment(), + width: self.private_get_fixed_key_size(), + }); + } + if self.private_get_fixed_value_size() != V::fixed_width() { + return Err(TableError::TypeDefinitionChanged { + name: V::type_name(), + alignment: self.private_get_value_alignment(), + width: self.private_get_fixed_value_size(), + }); + } + + Ok(()) + } + + pub(crate) fn visit_all_pages<'a, F>( + &self, + mem: Arc, + hint: PageHint, + visitor: F, + ) -> Result + where + F: FnMut(&PagePath) -> Result + 'a, + { + match self { + InternalTableDefinition::Normal { + table_root, + fixed_key_size, + fixed_value_size, + .. + } => { + let tree = + UntypedBtree::new(*table_root, mem, hint, *fixed_key_size, *fixed_value_size); + tree.visit_all_pages(visitor)?; + } + InternalTableDefinition::Multimap { + table_root, + fixed_key_size, + fixed_value_size, + .. + } => { + let tree = UntypedMultiBtree::new( + *table_root, + mem, + hint, + *fixed_key_size, + *fixed_value_size, + ); + tree.visit_all_pages(visitor)?; + } + } + + Ok(()) + } + + pub(crate) fn relocate_tree( + &mut self, + mem: Arc, + freed_pages: Arc>>, + relocation_map: &HashMap, + ) -> Result> { + let original_root = self.private_get_root(); + let relocated_root = match self { + InternalTableDefinition::Normal { table_root, .. } => *table_root, + InternalTableDefinition::Multimap { + table_root, + fixed_key_size, + fixed_value_size, + .. + } => { + if let Some(header) = table_root { + let (page_number, checksum) = relocate_subtrees( + (header.root, header.checksum), + *fixed_key_size, + *fixed_value_size, + mem.clone(), + freed_pages.clone(), + relocation_map, + )?; + Some(BtreeHeader::new(page_number, checksum, header.length)) + } else { + None + } + } + }; + let mut tree = UntypedBtreeMut::new( + relocated_root, + mem, + freed_pages, + self.private_get_fixed_key_size(), + self.private_get_fixed_value_size(), + ); + tree.relocate(relocation_map)?; + if tree.get_root() != original_root { + self.set_header(tree.get_root(), self.get_length()); + Ok(tree.get_root()) + } else { + Ok(None) + } + } + + fn private_get_root(&self) -> Option { + match self { + InternalTableDefinition::Normal { table_root, .. } + | InternalTableDefinition::Multimap { table_root, .. } => *table_root, + } + } + + pub(crate) fn get_length(&self) -> u64 { + match self { + InternalTableDefinition::Normal { table_length, .. } + | InternalTableDefinition::Multimap { table_length, .. } => *table_length, + } + } + + fn private_get_fixed_key_size(&self) -> Option { + match self { + InternalTableDefinition::Normal { fixed_key_size, .. } + | InternalTableDefinition::Multimap { fixed_key_size, .. } => *fixed_key_size, + } + } + + fn private_get_fixed_value_size(&self) -> Option { + match self { + InternalTableDefinition::Normal { + fixed_value_size, .. + } + | InternalTableDefinition::Multimap { + fixed_value_size, .. + } => *fixed_value_size, + } + } + + fn private_get_key_alignment(&self) -> usize { + match self { + InternalTableDefinition::Normal { key_alignment, .. } + | InternalTableDefinition::Multimap { key_alignment, .. } => *key_alignment, + } + } + + fn private_get_value_alignment(&self) -> usize { + match self { + InternalTableDefinition::Normal { + value_alignment, .. + } + | InternalTableDefinition::Multimap { + value_alignment, .. + } => *value_alignment, + } + } + + pub(crate) fn get_type(&self) -> TableType { + match self { + InternalTableDefinition::Normal { .. } => TableType::Normal, + InternalTableDefinition::Multimap { .. } => TableType::Multimap, + } + } + + fn private_key_type(&self) -> TypeName { + match self { + InternalTableDefinition::Normal { key_type, .. } + | InternalTableDefinition::Multimap { key_type, .. } => key_type.clone(), + } + } + + fn private_value_type(&self) -> TypeName { + match self { + InternalTableDefinition::Normal { value_type, .. } + | InternalTableDefinition::Multimap { value_type, .. } => value_type.clone(), + } + } +} + +impl Value for InternalTableDefinition { + type SelfType<'a> = InternalTableDefinition; + type AsBytes<'a> = Vec; + + fn fixed_width() -> Option { + None + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self + where + Self: 'a, + { + debug_assert!(data.len() > 22); + let mut offset = 0; + let legacy = TableType::is_legacy(data[offset]); + assert!(!legacy); + let table_type = TableType::from(data[offset]); + offset += 1; + + let table_length = u64::from_le_bytes( + data[offset..(offset + size_of::())] + .try_into() + .unwrap(), + ); + offset += size_of::(); + + let non_null = data[offset] != 0; + offset += 1; + let table_root = if non_null { + Some(BtreeHeader::from_le_bytes( + data[offset..(offset + BtreeHeader::serialized_size())] + .try_into() + .unwrap(), + )) + } else { + None + }; + offset += BtreeHeader::serialized_size(); + + let non_null = data[offset] != 0; + offset += 1; + let fixed_key_size = if non_null { + let fixed = u32::from_le_bytes( + data[offset..(offset + size_of::())] + .try_into() + .unwrap(), + ) as usize; + Some(fixed) + } else { + None + }; + offset += size_of::(); + + let non_null = data[offset] != 0; + offset += 1; + let fixed_value_size = if non_null { + let fixed = u32::from_le_bytes( + data[offset..(offset + size_of::())] + .try_into() + .unwrap(), + ) as usize; + Some(fixed) + } else { + None + }; + offset += size_of::(); + let key_alignment = u32::from_le_bytes( + data[offset..(offset + size_of::())] + .try_into() + .unwrap(), + ) as usize; + offset += size_of::(); + let value_alignment = u32::from_le_bytes( + data[offset..(offset + size_of::())] + .try_into() + .unwrap(), + ) as usize; + offset += size_of::(); + + let key_type_len = u32::from_le_bytes( + data[offset..(offset + size_of::())] + .try_into() + .unwrap(), + ) as usize; + offset += size_of::(); + let key_type = TypeName::from_bytes(&data[offset..(offset + key_type_len)]); + offset += key_type_len; + let value_type = TypeName::from_bytes(&data[offset..]); + + match table_type { + TableType::Normal => InternalTableDefinition::Normal { + table_root, + table_length, + fixed_key_size, + fixed_value_size, + key_alignment, + value_alignment, + key_type, + value_type, + }, + TableType::Multimap => InternalTableDefinition::Multimap { + table_root, + table_length, + fixed_key_size, + fixed_value_size, + key_alignment, + value_alignment, + key_type, + value_type, + }, + } + } + + // Be careful if you change this serialization format! The InternalTableDefinition for + // a given table needs to have a consistent serialized length, regardless of the table + // contents, so that create_table_and_flush_table_root() can update the allocator state + // table without causing more allocations + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Vec + where + Self: 'b, + { + let mut result = vec![value.get_type().into()]; + result.extend_from_slice(&value.get_length().to_le_bytes()); + if let Some(header) = value.private_get_root() { + result.push(1); + result.extend_from_slice(&header.to_le_bytes()); + } else { + result.push(0); + result.extend_from_slice(&[0; BtreeHeader::serialized_size()]); + } + if let Some(fixed) = value.private_get_fixed_key_size() { + result.push(1); + result.extend_from_slice(&u32::try_from(fixed).unwrap().to_le_bytes()); + } else { + result.push(0); + result.extend_from_slice(&[0; size_of::()]); + } + if let Some(fixed) = value.private_get_fixed_value_size() { + result.push(1); + result.extend_from_slice(&u32::try_from(fixed).unwrap().to_le_bytes()); + } else { + result.push(0); + result.extend_from_slice(&[0; size_of::()]); + } + result.extend_from_slice( + &u32::try_from(value.private_get_key_alignment()) + .unwrap() + .to_le_bytes(), + ); + result.extend_from_slice( + &u32::try_from(value.private_get_value_alignment()) + .unwrap() + .to_le_bytes(), + ); + let key_type_bytes = value.private_key_type().to_bytes(); + result.extend_from_slice(&u32::try_from(key_type_bytes.len()).unwrap().to_le_bytes()); + result.extend_from_slice(&key_type_bytes); + result.extend_from_slice(&value.private_value_type().to_bytes()); + + result + } + + fn type_name() -> TypeName { + TypeName::internal("redb::InternalTableDefinition") + } +} diff --git a/v2/redb/src/tuple_types.rs b/v2/redb/src/tuple_types.rs new file mode 100644 index 000000000..3e7db3cfe --- /dev/null +++ b/v2/redb/src/tuple_types.rs @@ -0,0 +1,448 @@ +use crate::complex_types::{decode_varint_len, encode_varint_len}; +use crate::types::{Key, TypeName, Value}; +use std::borrow::Borrow; +use std::cmp::Ordering; + +fn serialize_tuple_elements_variable( + is_fixed_width: [bool; N], + slices: [&[u8]; N], +) -> Vec { + let total_len: usize = slices.iter().map(|x| x.len()).sum(); + let worst_case_len_overhead: usize = + is_fixed_width.iter().map(|x| if *x { 0 } else { 5 }).sum(); + let mut output = Vec::with_capacity(total_len + worst_case_len_overhead); + let zipped = is_fixed_width.iter().zip(slices.iter()); + for len in zipped + .map(|(fixed, x)| if *fixed { None } else { Some(x.len()) }) + .take(slices.len() - 1) + .flatten() + { + encode_varint_len(len, &mut output); + } + + for slice in slices { + output.extend_from_slice(slice); + } + + debug_assert!(output.len() <= total_len + worst_case_len_overhead); + + output +} + +fn serialize_tuple_elements_fixed(slices: &[&[u8]]) -> Vec { + let total_len: usize = slices.iter().map(|x| x.len()).sum(); + let mut output = Vec::with_capacity(total_len); + for slice in slices { + output.extend_from_slice(slice); + } + output +} + +fn parse_lens(fixed_width: [Option; N], data: &[u8]) -> (usize, [usize; N]) { + let mut result = [0; N]; + let mut offset = 0; + for (i, &fixed) in fixed_width.iter().enumerate() { + if let Some(len) = fixed { + result[i] = len; + } else { + let (len, bytes_read) = decode_varint_len(&data[offset..]); + result[i] = len; + offset += bytes_read; + } + } + (offset, result) +} + +fn not_equal(data1: &[u8], data2: &[u8]) -> Option { + match T::compare(data1, data2) { + Ordering::Less => Some(Ordering::Less), + Ordering::Equal => None, + Ordering::Greater => Some(Ordering::Greater), + } +} + +macro_rules! fixed_width_impl { + ( $( $t:ty ),+ ) => { + { + let mut sum = 0; + $( + sum += <$t>::fixed_width()?; + )+ + Some(sum) + } + }; +} + +macro_rules! as_bytes_impl { + ( $value:expr, $( $t:ty, $i:tt ),+ ) => {{ + if Self::fixed_width().is_some() { + serialize_tuple_elements_fixed(&[ + $( + <$t>::as_bytes($value.$i.borrow()).as_ref(), + )+ + ]) + } else { + serialize_tuple_elements_variable( + [ + $( + <$t>::fixed_width().is_some(), + )+ + ], + [ + $( + <$t>::as_bytes($value.$i.borrow()).as_ref(), + )+ + ]) + } + }}; +} + +macro_rules! type_name_impl { + ( $head:ty $(,$tail:ty)+ ) => { + { + let mut result = String::new(); + result.push('('); + result.push_str(&<$head>::type_name().name()); + $( + result.push(','); + result.push_str(&<$tail>::type_name().name()); + )+ + result.push(')'); + + if Self::fixed_width().is_some() { + TypeName::internal(&result) + } else { + TypeName::internal2(&result) + } + } + }; +} + +macro_rules! from_bytes_variable_impl { + ( $data:expr $(,$t:ty, $v:ident, $i:literal )+ | $t_last:ty, $v_last:ident, $i_last:literal ) => { + #[allow(clippy::manual_bits)] + { + let (mut offset, lens) = parse_lens::<$i_last>( + [ + $( + <$t>::fixed_width(), + )+ + ], + $data); + $( + let len = lens[$i]; + let $v = <$t>::from_bytes(&$data[offset..(offset + len)]); + offset += len; + )+ + let $v_last = <$t_last>::from_bytes(&$data[offset..]); + ($( + $v, + )+ + $v_last + ) + } + }; +} + +macro_rules! from_bytes_fixed_impl { + ( $data:expr $(,$t:ty, $v:ident )+ ) => { + { + let mut offset = 0; + $( + let len = <$t>::fixed_width().unwrap(); + let $v = <$t>::from_bytes(&$data[offset..(offset + len)]); + #[allow(unused_assignments)] + { + offset += len; + } + )+ + + ($( + $v, + )+) + } + }; +} + +macro_rules! compare_variable_impl { + ( $data0:expr, $data1:expr $(,$t:ty, $i:literal )+ | $t_last:ty, $i_last:literal ) => { + #[allow(clippy::manual_bits)] + { + let fixed_width = [ + $( + <$t>::fixed_width(), + )+ + ]; + let (mut offset0, lens0) = parse_lens::<$i_last>(fixed_width, $data0); + let (mut offset1, lens1) = parse_lens::<$i_last>(fixed_width, $data1); + $( + let index = $i; + let len0 = lens0[index]; + let len1 = lens1[index]; + if let Some(order) = not_equal::<$t>( + &$data0[offset0..(offset0 + len0)], + &$data1[offset1..(offset1 + len1)], + ) { + return order; + } + offset0 += len0; + offset1 += len1; + )+ + + <$t_last>::compare(&$data0[offset0..], &$data1[offset1..]) + } + }; +} + +macro_rules! compare_fixed_impl { + ( $data0:expr, $data1:expr, $($t:ty),+ ) => { + { + let mut offset0 = 0; + let mut offset1 = 0; + $( + let len = <$t>::fixed_width().unwrap(); + if let Some(order) = not_equal::<$t>( + &$data0[offset0..(offset0 + len)], + &$data1[offset1..(offset1 + len)], + ) { + return order; + } + #[allow(unused_assignments)] + { + offset0 += len; + offset1 += len; + } + )+ + + Ordering::Equal + } + }; +} + +macro_rules! tuple_impl { + ( $($t:ident, $v:ident, $i:tt ),+ | $t_last:ident, $v_last:ident, $i_last:tt ) => { + impl<$($t: Value,)+ $t_last: Value> Value for ($($t,)+ $t_last) { + type SelfType<'a> = ( + $(<$t>::SelfType<'a>,)+ + <$t_last>::SelfType<'a>, + ) + where + Self: 'a; + type AsBytes<'a> = Vec + where + Self: 'a; + + fn fixed_width() -> Option { + fixed_width_impl!($($t,)+ $t_last) + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + if Self::fixed_width().is_some() { + from_bytes_fixed_impl!(data $(,$t,$v)+, $t_last, $v_last) + } else { + from_bytes_variable_impl!(data $(,$t,$v,$i)+ | $t_last, $v_last, $i_last) + } + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Vec + where + Self: 'a, + Self: 'b, + { + as_bytes_impl!(value, $($t,$i,)+ $t_last, $i_last) + } + + fn type_name() -> TypeName { + type_name_impl!($($t,)+ $t_last) + } + } + + impl<$($t: Key,)+ $t_last: Key> Key for ($($t,)+ $t_last) { + fn compare(data1: &[u8], data2: &[u8]) -> Ordering { + if Self::fixed_width().is_some() { + compare_fixed_impl!(data1, data2, $($t,)+ $t_last) + } else { + compare_variable_impl!(data1, data2 $(,$t,$i)+ | $t_last, $i_last) + } + } + } + }; +} + +impl Value for (T,) { + type SelfType<'a> + = (T::SelfType<'a>,) + where + Self: 'a; + type AsBytes<'a> + = T::AsBytes<'a> + where + Self: 'a; + + fn fixed_width() -> Option { + T::fixed_width() + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + (T::from_bytes(data),) + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'a, + Self: 'b, + { + T::as_bytes(&value.0) + } + + fn type_name() -> TypeName { + TypeName::internal(&format!("({},)", T::type_name().name())) + } +} + +impl Key for (T,) { + fn compare(data1: &[u8], data2: &[u8]) -> Ordering { + T::compare(data1, data2) + } +} + +tuple_impl! { + T0, t0, 0 + | T1, t1, 1 +} + +tuple_impl! { + T0, t0, 0, + T1, t1, 1 + | T2, t2, 2 +} + +tuple_impl! { + T0, t0, 0, + T1, t1, 1, + T2, t2, 2 + | T3, t3, 3 +} + +tuple_impl! { + T0, t0, 0, + T1, t1, 1, + T2, t2, 2, + T3, t3, 3 + | T4, t4, 4 +} + +tuple_impl! { + T0, t0, 0, + T1, t1, 1, + T2, t2, 2, + T3, t3, 3, + T4, t4, 4 + | T5, t5, 5 +} + +tuple_impl! { + T0, t0, 0, + T1, t1, 1, + T2, t2, 2, + T3, t3, 3, + T4, t4, 4, + T5, t5, 5 + | T6, t6, 6 +} + +tuple_impl! { + T0, t0, 0, + T1, t1, 1, + T2, t2, 2, + T3, t3, 3, + T4, t4, 4, + T5, t5, 5, + T6, t6, 6 + | T7, t7, 7 +} + +tuple_impl! { + T0, t0, 0, + T1, t1, 1, + T2, t2, 2, + T3, t3, 3, + T4, t4, 4, + T5, t5, 5, + T6, t6, 6, + T7, t7, 7 + | T8, t8, 8 +} + +tuple_impl! { + T0, t0, 0, + T1, t1, 1, + T2, t2, 2, + T3, t3, 3, + T4, t4, 4, + T5, t5, 5, + T6, t6, 6, + T7, t7, 7, + T8, t8, 8 + | T9, t9, 9 +} + +tuple_impl! { + T0, t0, 0, + T1, t1, 1, + T2, t2, 2, + T3, t3, 3, + T4, t4, 4, + T5, t5, 5, + T6, t6, 6, + T7, t7, 7, + T8, t8, 8, + T9, t9, 9 + | T10, t10, 10 +} + +tuple_impl! { + T0, t0, 0, + T1, t1, 1, + T2, t2, 2, + T3, t3, 3, + T4, t4, 4, + T5, t5, 5, + T6, t6, 6, + T7, t7, 7, + T8, t8, 8, + T9, t9, 9, + T10, t10, 10 + | T11, t11, 11 +} + +#[cfg(test)] +mod test { + use crate::types::Value; + + #[test] + fn width() { + assert!(<(&str, u8)>::fixed_width().is_none()); + assert!(<(u16, u8, &str, u128)>::fixed_width().is_none()); + assert_eq!(<(u16,)>::fixed_width().unwrap(), 2); + assert_eq!(<(u16, u8)>::fixed_width().unwrap(), 3); + assert_eq!(<(u16, u8, u128)>::fixed_width().unwrap(), 19); + assert_eq!(<(u16, u8, i8, u128)>::fixed_width().unwrap(), 20); + // Check that length of final field is elided + assert_eq!( + <(u8, &str)>::as_bytes(&(1, "hello")).len(), + "hello".len() + size_of::() + ); + // Check that varint encoding uses only 1 byte for small strings + assert_eq!( + <(&str, u8)>::as_bytes(&("hello", 1)).len(), + "hello".len() + size_of::() + size_of::() + ); + } +} diff --git a/v2/redb/src/types.rs b/v2/redb/src/types.rs new file mode 100644 index 000000000..09dd36a6a --- /dev/null +++ b/v2/redb/src/types.rs @@ -0,0 +1,665 @@ +use std::cmp::Ordering; +use std::convert::TryInto; +use std::fmt::Debug; +use std::mem::size_of; +#[cfg(feature = "chrono_v0_4")] +mod chrono_v0_4; +#[cfg(feature = "uuid")] +mod uuid; + +#[derive(Eq, PartialEq, Clone, Debug)] +enum TypeClassification { + Internal, + UserDefined, + // Used by variable width tuple encoding in version 3.0 and newer. This differentiates the encoding + // from the old encoding used previously + Internal2, +} + +impl TypeClassification { + fn to_byte(&self) -> u8 { + match self { + TypeClassification::Internal => 1, + TypeClassification::UserDefined => 2, + TypeClassification::Internal2 => 3, + } + } + + fn from_byte(value: u8) -> Self { + match value { + 1 => TypeClassification::Internal, + 2 => TypeClassification::UserDefined, + 3 => TypeClassification::Internal2, + _ => unreachable!(), + } + } +} + +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct TypeName { + classification: TypeClassification, + name: String, +} + +impl TypeName { + /// It is recommended that `name` be prefixed with the crate name to minimize the chance of + /// it coliding with another user defined type + pub fn new(name: &str) -> Self { + Self { + classification: TypeClassification::UserDefined, + name: name.to_string(), + } + } + + pub(crate) fn internal(name: &str) -> Self { + Self { + classification: TypeClassification::Internal, + name: name.to_string(), + } + } + + pub(crate) fn internal2(name: &str) -> Self { + Self { + classification: TypeClassification::Internal2, + name: name.to_string(), + } + } + + pub(crate) fn to_bytes(&self) -> Vec { + let mut result = Vec::with_capacity(self.name.len() + 1); + result.push(self.classification.to_byte()); + result.extend_from_slice(self.name.as_bytes()); + result + } + + pub(crate) fn from_bytes(bytes: &[u8]) -> Self { + let classification = TypeClassification::from_byte(bytes[0]); + let name = std::str::from_utf8(&bytes[1..]).unwrap().to_string(); + + Self { + classification, + name, + } + } + + pub fn name(&self) -> &str { + &self.name + } +} + +/// Types that implement this trait can be used as values in a redb table +pub trait Value: Debug { + /// `SelfType<'a>` must be the same type as Self with all lifetimes replaced with 'a + type SelfType<'a>: Debug + 'a + where + Self: 'a; + + type AsBytes<'a>: AsRef<[u8]> + 'a + where + Self: 'a; + + /// Width of a fixed type, or None for variable width + fn fixed_width() -> Option; + + /// Deserializes data + /// Implementations may return a view over data, or an owned type + /// + /// Note: Implementations may assume that `data` is the return value of `as_bytes(&v)` for some `v` of type `Self`. + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a; + + /// Serialize the value to a slice + /// + /// Note: Implementations must ensure that `as_bytes()` and `from_bytes()` are inverses. + /// Specifically, for any value `v` and `data` returned from `as_bytes(&v)`, + /// `as_bytes(from_bytes(data))` must equal `data`. + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'b; + + /// Globally unique identifier for this type + fn type_name() -> TypeName; +} + +/// Implementing this trait indicates that the type can be mutated in-place as a &mut [u8]. +/// This enables the `.insert_reserve()` method on Table +pub trait MutInPlaceValue: Value { + /// The base type such that &mut [u8] can be safely transmuted to `&mut BaseRefType` + type BaseRefType: Debug + ?Sized; + + /// Initialize `data` to a valid value. This method will be called (at some point, not necessarily immediately) + /// before `from_bytes_mut()` is called on a slice. + /// + /// Note: There must exist a value `v` of type `Self` such that `as_bytes(&v)` equals `data`. + fn initialize(data: &mut [u8]); + + fn from_bytes_mut(data: &mut [u8]) -> &mut Self::BaseRefType; +} + +impl MutInPlaceValue for &[u8] { + type BaseRefType = [u8]; + + fn initialize(_data: &mut [u8]) { + // no-op. All values are valid. + } + + fn from_bytes_mut(data: &mut [u8]) -> &mut Self::BaseRefType { + data + } +} + +/// Trait which allows the type to be used as a key in a redb table +pub trait Key: Value { + /// Compare data1 with data2. + /// + /// The implementation must ensure there is a total order + fn compare(data1: &[u8], data2: &[u8]) -> Ordering; +} + +impl Value for () { + type SelfType<'a> + = () + where + Self: 'a; + type AsBytes<'a> + = &'a [u8] + where + Self: 'a; + + fn fixed_width() -> Option { + Some(0) + } + + #[allow(clippy::unused_unit, clippy::semicolon_if_nothing_returned)] + fn from_bytes<'a>(_data: &'a [u8]) -> () + where + Self: 'a, + { + () + } + + #[allow(clippy::ignored_unit_patterns)] + fn as_bytes<'a, 'b: 'a>(_: &'a Self::SelfType<'b>) -> &'a [u8] + where + Self: 'b, + { + &[] + } + + fn type_name() -> TypeName { + TypeName::internal("()") + } +} + +impl Key for () { + fn compare(_data1: &[u8], _data2: &[u8]) -> Ordering { + Ordering::Equal + } +} + +impl Value for bool { + type SelfType<'a> + = bool + where + Self: 'a; + type AsBytes<'a> + = &'a [u8] + where + Self: 'a; + + fn fixed_width() -> Option { + Some(1) + } + + fn from_bytes<'a>(data: &'a [u8]) -> bool + where + Self: 'a, + { + match data[0] { + 0 => false, + 1 => true, + _ => unreachable!(), + } + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> &'a [u8] + where + Self: 'b, + { + match value { + true => &[1], + false => &[0], + } + } + + fn type_name() -> TypeName { + TypeName::internal("bool") + } +} + +impl Key for bool { + fn compare(data1: &[u8], data2: &[u8]) -> Ordering { + let value1 = Self::from_bytes(data1); + let value2 = Self::from_bytes(data2); + value1.cmp(&value2) + } +} + +impl Value for Option { + type SelfType<'a> + = Option> + where + Self: 'a; + type AsBytes<'a> + = Vec + where + Self: 'a; + + fn fixed_width() -> Option { + T::fixed_width().map(|x| x + 1) + } + + fn from_bytes<'a>(data: &'a [u8]) -> Option> + where + Self: 'a, + { + match data[0] { + 0 => None, + 1 => Some(T::from_bytes(&data[1..])), + _ => unreachable!(), + } + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Vec + where + Self: 'b, + { + let mut result = vec![0]; + if let Some(x) = value { + result[0] = 1; + result.extend_from_slice(T::as_bytes(x).as_ref()); + } else if let Some(fixed_width) = T::fixed_width() { + result.extend_from_slice(&vec![0; fixed_width]); + } + result + } + + fn type_name() -> TypeName { + TypeName::internal(&format!("Option<{}>", T::type_name().name())) + } +} + +impl Key for Option { + #[allow(clippy::collapsible_else_if)] + fn compare(data1: &[u8], data2: &[u8]) -> Ordering { + if data1[0] == 0 { + if data2[0] == 0 { + Ordering::Equal + } else { + Ordering::Less + } + } else { + if data2[0] == 0 { + Ordering::Greater + } else { + T::compare(&data1[1..], &data2[1..]) + } + } + } +} + +impl Value for &[u8] { + type SelfType<'a> + = &'a [u8] + where + Self: 'a; + type AsBytes<'a> + = &'a [u8] + where + Self: 'a; + + fn fixed_width() -> Option { + None + } + + fn from_bytes<'a>(data: &'a [u8]) -> &'a [u8] + where + Self: 'a, + { + data + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> &'a [u8] + where + Self: 'b, + { + value + } + + fn type_name() -> TypeName { + TypeName::internal("&[u8]") + } +} + +impl Key for &[u8] { + fn compare(data1: &[u8], data2: &[u8]) -> Ordering { + data1.cmp(data2) + } +} + +impl Value for &[u8; N] { + type SelfType<'a> + = &'a [u8; N] + where + Self: 'a; + type AsBytes<'a> + = &'a [u8; N] + where + Self: 'a; + + fn fixed_width() -> Option { + Some(N) + } + + fn from_bytes<'a>(data: &'a [u8]) -> &'a [u8; N] + where + Self: 'a, + { + data.try_into().unwrap() + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> &'a [u8; N] + where + Self: 'b, + { + value + } + + fn type_name() -> TypeName { + TypeName::internal(&format!("[u8;{N}]")) + } +} + +impl Key for &[u8; N] { + fn compare(data1: &[u8], data2: &[u8]) -> Ordering { + data1.cmp(data2) + } +} + +impl Value for [T; N] { + type SelfType<'a> + = [T::SelfType<'a>; N] + where + Self: 'a; + type AsBytes<'a> + = Vec + where + Self: 'a; + + fn fixed_width() -> Option { + T::fixed_width().map(|x| x * N) + } + + fn from_bytes<'a>(data: &'a [u8]) -> [T::SelfType<'a>; N] + where + Self: 'a, + { + let mut result = Vec::with_capacity(N); + if let Some(fixed) = T::fixed_width() { + for i in 0..N { + result.push(T::from_bytes(&data[fixed * i..fixed * (i + 1)])); + } + } else { + // Set offset to the first data item + let mut start = size_of::() * N; + for i in 0..N { + let range = size_of::() * i..size_of::() * (i + 1); + let end = u32::from_le_bytes(data[range].try_into().unwrap()) as usize; + result.push(T::from_bytes(&data[start..end])); + start = end; + } + } + result.try_into().unwrap() + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Vec + where + Self: 'b, + { + if let Some(fixed) = T::fixed_width() { + let mut result = Vec::with_capacity(fixed * N); + for item in value { + result.extend_from_slice(T::as_bytes(item).as_ref()); + } + result + } else { + // Reserve space for the end offsets + let mut result = vec![0u8; size_of::() * N]; + for i in 0..N { + result.extend_from_slice(T::as_bytes(&value[i]).as_ref()); + let end: u32 = result.len().try_into().unwrap(); + result[size_of::() * i..size_of::() * (i + 1)] + .copy_from_slice(&end.to_le_bytes()); + } + result + } + } + + fn type_name() -> TypeName { + // Uses the same type name as [T;N] so that tables are compatible with [u8;N] and &[u8;N] types + // This requires that the binary encoding be the same + TypeName::internal(&format!("[{};{N}]", T::type_name().name())) + } +} + +impl Key for [T; N] { + fn compare(data1: &[u8], data2: &[u8]) -> Ordering { + if let Some(fixed) = T::fixed_width() { + for i in 0..N { + let range = fixed * i..fixed * (i + 1); + let comparison = T::compare(&data1[range.clone()], &data2[range]); + if !comparison.is_eq() { + return comparison; + } + } + } else { + // Set offset to the first data item + let mut start1 = size_of::() * N; + let mut start2 = size_of::() * N; + for i in 0..N { + let range = size_of::() * i..size_of::() * (i + 1); + let end1 = u32::from_le_bytes(data1[range.clone()].try_into().unwrap()) as usize; + let end2 = u32::from_le_bytes(data2[range].try_into().unwrap()) as usize; + let comparison = T::compare(&data1[start1..end1], &data2[start2..end2]); + if !comparison.is_eq() { + return comparison; + } + start1 = end1; + start2 = end2; + } + } + Ordering::Equal + } +} + +impl Value for &str { + type SelfType<'a> + = &'a str + where + Self: 'a; + type AsBytes<'a> + = &'a str + where + Self: 'a; + + fn fixed_width() -> Option { + None + } + + fn from_bytes<'a>(data: &'a [u8]) -> &'a str + where + Self: 'a, + { + std::str::from_utf8(data).unwrap() + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> &'a str + where + Self: 'b, + { + value + } + + fn type_name() -> TypeName { + TypeName::internal("&str") + } +} + +impl Key for &str { + fn compare(data1: &[u8], data2: &[u8]) -> Ordering { + let str1 = Self::from_bytes(data1); + let str2 = Self::from_bytes(data2); + str1.cmp(str2) + } +} + +impl Value for String { + type SelfType<'a> + = String + where + Self: 'a; + type AsBytes<'a> + = &'a str + where + Self: 'a; + + fn fixed_width() -> Option { + None + } + + fn from_bytes<'a>(data: &'a [u8]) -> String + where + Self: 'a, + { + std::str::from_utf8(data).unwrap().to_string() + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> &'a str + where + Self: 'b, + { + value.as_str() + } + + fn type_name() -> TypeName { + TypeName::internal("String") + } +} + +impl Key for String { + fn compare(data1: &[u8], data2: &[u8]) -> Ordering { + let str1 = std::str::from_utf8(data1).unwrap(); + let str2 = std::str::from_utf8(data2).unwrap(); + str1.cmp(str2) + } +} + +impl Value for char { + type SelfType<'a> = char; + type AsBytes<'a> + = [u8; 3] + where + Self: 'a; + + fn fixed_width() -> Option { + Some(3) + } + + fn from_bytes<'a>(data: &'a [u8]) -> char + where + Self: 'a, + { + char::from_u32(u32::from_le_bytes([data[0], data[1], data[2], 0])).unwrap() + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> [u8; 3] + where + Self: 'b, + { + let bytes = u32::from(*value).to_le_bytes(); + [bytes[0], bytes[1], bytes[2]] + } + + fn type_name() -> TypeName { + TypeName::internal(stringify!(char)) + } +} + +impl Key for char { + fn compare(data1: &[u8], data2: &[u8]) -> Ordering { + Self::from_bytes(data1).cmp(&Self::from_bytes(data2)) + } +} + +macro_rules! le_value { + ($t:ty) => { + impl Value for $t { + type SelfType<'a> = $t; + type AsBytes<'a> + = [u8; std::mem::size_of::<$t>()] + where + Self: 'a; + + fn fixed_width() -> Option { + Some(std::mem::size_of::<$t>()) + } + + fn from_bytes<'a>(data: &'a [u8]) -> $t + where + Self: 'a, + { + <$t>::from_le_bytes(data.try_into().unwrap()) + } + + fn as_bytes<'a, 'b: 'a>( + value: &'a Self::SelfType<'b>, + ) -> [u8; std::mem::size_of::<$t>()] + where + Self: 'a, + Self: 'b, + { + value.to_le_bytes() + } + + fn type_name() -> TypeName { + TypeName::internal(stringify!($t)) + } + } + }; +} + +macro_rules! le_impl { + ($t:ty) => { + le_value!($t); + + impl Key for $t { + fn compare(data1: &[u8], data2: &[u8]) -> Ordering { + Self::from_bytes(data1).cmp(&Self::from_bytes(data2)) + } + } + }; +} + +le_impl!(u8); +le_impl!(u16); +le_impl!(u32); +le_impl!(u64); +le_impl!(u128); +le_impl!(i8); +le_impl!(i16); +le_impl!(i32); +le_impl!(i64); +le_impl!(i128); +le_value!(f32); +le_value!(f64); diff --git a/v2/redb/src/types/chrono_v0_4.rs b/v2/redb/src/types/chrono_v0_4.rs new file mode 100644 index 000000000..a00115dbd --- /dev/null +++ b/v2/redb/src/types/chrono_v0_4.rs @@ -0,0 +1,547 @@ +//! All of the structures in this module can be serialized to a fixed-width byte array. +//! +//! They are designed to be compatible with any time implemenation. All integers are using little endian. The layout of the memory is as follows: +//! +//!| Structure | Layout | Size | | +//!|-----------------------|--------------------------------------------------------------------------------------------------------------------------|------|--------------------------------------------------| +//!| NaiveDate | `{year:i32}\|{month:u8}\|{day:u8}` | 6 | | +//!| NaiveTime | `{seconds_from_midnight:u32:first 3 bytes}\|{nanoseconds:u32}` | 7 | | +//!| NaiveDateTime | `{year:i32}\|{month:u8}\|{day:u8}\|{seconds_from_midnight:u32:first 3 bytes}\|{nanoseconds:u32}` | 13 | | +//!| FixedOffset | `{seconds_from_utc:i32}` | 4 | | +//!| DateTime | `{year:i32}\|{month:u8}\|{day:u8}\|{seconds_from_midnight:u32:first 3 bytes}\|{nanoseconds:u32}\|{seconds_from_utc:i32}` | 17 | Time is stored in UTC with the offset in seconds | +use crate::{Key, TypeName, Value}; + +use chrono_v0_4::{ + DateTime, Datelike, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Timelike, Utc, +}; +/// A 6-byte representation of a date in the format `{year:i32}\|{month:u8}\|{day:u8}`. +impl Value for NaiveDate { + type SelfType<'a> + = NaiveDate + where + Self: 'a; + + type AsBytes<'a> + = [u8; 6] + where + Self: 'a; + + fn fixed_width() -> Option { + Some(6) + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + assert_eq!( + data.len(), + 6, + "NaiveDate must be 6 bytes long, got {}", + data.len() + ); + date_from_bytes(data) + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'b, + { + let year = value.year().to_le_bytes(); + let month = u8::try_from(value.month()).unwrap(); + let day = u8::try_from(value.day()).unwrap(); + [year[0], year[1], year[2], year[3], month, day] + } + + fn type_name() -> super::TypeName { + TypeName::new("chrono::NaiveDate") + } +} +impl Key for NaiveDate { + fn compare(data1: &[u8], data2: &[u8]) -> std::cmp::Ordering { + let date1 = date_from_bytes(data1); + let date2 = date_from_bytes(data2); + date1.cmp(&date2) + } +} +/// A 7-byte representation of a time in the format `{seconds_from_midnight:i32:first 3 bytes}\|{nanoseconds:i32}`. +/// +/// Only the first 3 bytes of the seconds from midnight are stored, as the maximum value is 86,400 seconds (24 hours). +impl Value for NaiveTime { + type SelfType<'a> + = NaiveTime + where + Self: 'a; + type AsBytes<'a> + = [u8; 7] + where + Self: 'a; + + fn fixed_width() -> Option { + Some(7) + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + assert!( + data.len() == 7, + "NaiveTime must be 7 bytes long, got {}", + data.len() + ); + time_from_bytes(data) + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'b, + { + let num_seconds = value.num_seconds_from_midnight().to_le_bytes(); + let num_nanoseconds = value.nanosecond().to_le_bytes(); + [ + num_seconds[0], + num_seconds[1], + num_seconds[2], + num_nanoseconds[0], + num_nanoseconds[1], + num_nanoseconds[2], + num_nanoseconds[3], + ] + } + + fn type_name() -> TypeName { + TypeName::new("chrono::NaiveTime") + } +} +impl Key for NaiveTime { + fn compare(data1: &[u8], data2: &[u8]) -> std::cmp::Ordering { + let time1 = time_from_bytes(data1); + let time2 = time_from_bytes(data2); + time1.cmp(&time2) + } +} +/// A 13-byte representation of a date and time in the format `{year:i32}\|{month:u8}\|{day:u8}\|{seconds_from_midnight:i32:first 3 bytes}\|{nanoseconds:i32}`. +/// The date is stored as a `NaiveDate` and the time as a `NaiveTime`. The maximum value for seconds from midnight is 86,400 seconds (24 hours), so only the first 3 bytes are stored. +impl Value for NaiveDateTime { + type SelfType<'a> + = NaiveDateTime + where + Self: 'a; + + type AsBytes<'a> + = [u8; 13] + where + Self: 'a; + + fn fixed_width() -> Option { + Some(13) + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + assert_eq!( + data.len(), + 13, + "NaiveDateTime must be 13 bytes long, got {}", + data.len() + ); + + let date = date_from_bytes(&data[0..6]); + let time = time_from_bytes(&data[6..13]); + NaiveDateTime::new(date, time) + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'b, + { + let year = value.year().to_le_bytes(); + let month = u8::try_from(value.month()).unwrap(); + let day = u8::try_from(value.day()).unwrap(); + let time_since_midnight = value.time().num_seconds_from_midnight().to_le_bytes(); + let nanoseconds = value.time().nanosecond().to_le_bytes(); + + [ + year[0], + year[1], + year[2], + year[3], + month, + day, + time_since_midnight[0], + time_since_midnight[1], + time_since_midnight[2], + nanoseconds[0], + nanoseconds[1], + nanoseconds[2], + nanoseconds[3], + ] + } + + fn type_name() -> super::TypeName { + TypeName::new("chrono::NaiveDateTime") + } +} +impl Key for NaiveDateTime { + fn compare(data1: &[u8], data2: &[u8]) -> std::cmp::Ordering { + let date_time1 = NaiveDateTime::from_bytes(data1); + let date_time2 = NaiveDateTime::from_bytes(data2); + date_time1.cmp(&date_time2) + } +} + +impl Value for DateTime { + type SelfType<'a> + = DateTime + where + Self: 'a; + + type AsBytes<'a> + = [u8; 17] + where + Self: 'a; + + fn fixed_width() -> Option { + Some(17) + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + assert_eq!( + data.len(), + 17, + "DateTime must be 17 bytes long, got {}", + data.len() + ); + let date = date_from_bytes(&data[0..6]); + let time = time_from_bytes(&data[6..13]); + + let offset = offset_from_bytes(&data[13..17]); + let time = NaiveDateTime::new(date, time); + offset.from_utc_datetime(&time) + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'b, + { + let value_utc = value.with_timezone(&Utc); + let year = value_utc.year().to_le_bytes(); + let month = u8::try_from(value_utc.month()).unwrap(); + let day = u8::try_from(value_utc.day()).unwrap(); + let time_since_midnight = value_utc.time().num_seconds_from_midnight().to_le_bytes(); + + let nanoseconds = value_utc.time().nanosecond().to_le_bytes(); + + let offset_seconds = value.offset().local_minus_utc().to_le_bytes(); + [ + year[0], + year[1], + year[2], + year[3], + month, + day, + time_since_midnight[0], + time_since_midnight[1], + time_since_midnight[2], + nanoseconds[0], + nanoseconds[1], + nanoseconds[2], + nanoseconds[3], + offset_seconds[0], + offset_seconds[1], + offset_seconds[2], + offset_seconds[3], + ] + } + + fn type_name() -> TypeName { + TypeName::new("chrono::DateTime") + } +} +impl Key for DateTime { + fn compare(data1: &[u8], data2: &[u8]) -> std::cmp::Ordering { + let datetime1 = DateTime::::from_bytes(data1); + let datetime2 = DateTime::::from_bytes(data2); + datetime1.cmp(&datetime2) + } +} +impl Value for FixedOffset { + type SelfType<'a> + = FixedOffset + where + Self: 'a; + + type AsBytes<'a> + = [u8; 4] + where + Self: 'a; + + fn fixed_width() -> Option { + Some(4) + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + assert_eq!( + data.len(), + 4, + "FixedOffset must be 4 bytes long got {}", + data.len() + ); + offset_from_bytes(data) + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'b, + { + value.local_minus_utc().to_le_bytes() + } + + fn type_name() -> TypeName { + TypeName::new("chrono::FixedOffset") + } +} +impl Key for FixedOffset { + fn compare(data1: &[u8], data2: &[u8]) -> std::cmp::Ordering { + let offset1 = FixedOffset::from_bytes(data1); + let offset2 = FixedOffset::from_bytes(data2); + offset1.local_minus_utc().cmp(&offset2.local_minus_utc()) + } +} +fn offset_from_bytes(data: &[u8]) -> FixedOffset { + let offset_seconds = i32::from_le_bytes(data.try_into().unwrap()); + FixedOffset::east_opt(offset_seconds) + .expect("Invalid offset seconds, must be between -86400 and 86400") +} +fn date_from_bytes(data: &[u8]) -> NaiveDate { + let year = i32::from_le_bytes(data[0..4].try_into().unwrap()); + let month = u32::from(data[4]); + let day = u32::from(data[5]); + NaiveDate::from_ymd_opt(year, month, day).expect("Invalid date") +} +fn time_from_bytes(data: &[u8]) -> NaiveTime { + let num_seconds = u32::from_le_bytes([data[0], data[1], data[2], 0]); + let num_nanoseconds = u32::from_le_bytes(data[3..7].try_into().unwrap()); + NaiveTime::from_num_seconds_from_midnight_opt(num_seconds, num_nanoseconds) + .expect("Invalid time") +} + +#[cfg(test)] +mod tests { + use crate::{Database, Key, ReadableDatabase, TableDefinition, Value}; + use chrono_v0_4::{ + DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, + }; + use tempfile::NamedTempFile; + const NAIVE_DATE_TABLE: TableDefinition = + TableDefinition::new("naive_date_table"); + const NAIVE_TIME_TABLE: TableDefinition = + TableDefinition::new("naive_time_table"); + const NAIVE_DATETIME_TABLE: TableDefinition = + TableDefinition::new("naive_datetime_table"); + const FIXED_OFFSET_TABLE: TableDefinition = + TableDefinition::new("fixed_offset_table"); + const DATETIME_FIXED_OFFSET_TABLE: TableDefinition, i32> = + TableDefinition::new("datetime_fixed_offset_table"); + #[test] + fn test_naive_date() { + let date = NaiveDate::from_ymd_opt(2023, 10, 5).unwrap(); + let bytes = NaiveDate::as_bytes(&date); + assert_eq!( + NaiveDate::fixed_width(), + Some(bytes.len()), + "NaiveDate should have fixed width" + ); + assert_eq!( + NaiveDate::compare(&bytes, &bytes), + std::cmp::Ordering::Equal, + "Bytes should compare equal to themselves" + ); + let date_from_bytes = NaiveDate::from_bytes(&bytes); + assert_eq!(date, date_from_bytes); + } + #[test] + fn test_naive_time() { + let time = NaiveTime::from_hms_opt(12, 30, 45).unwrap(); + let bytes = NaiveTime::as_bytes(&time); + assert_eq!( + NaiveTime::fixed_width(), + Some(bytes.len()), + "NaiveTime should have fixed width" + ); + assert_eq!( + NaiveTime::compare(&bytes, &bytes), + std::cmp::Ordering::Equal, + "Bytes should compare equal to themselves" + ); + let time_from_bytes = NaiveTime::from_bytes(&bytes); + assert_eq!(time, time_from_bytes); + } + #[test] + fn test_naive_date_time() { + let date = NaiveDate::from_ymd_opt(2023, 10, 5).unwrap(); + let time = NaiveTime::from_hms_opt(12, 30, 45).unwrap(); + let datetime = NaiveDateTime::new(date, time); + let bytes = NaiveDateTime::as_bytes(&datetime); + assert_eq!( + NaiveDateTime::fixed_width(), + Some(bytes.len()), + "NaiveDateTime should have fixed width" + ); + assert_eq!( + NaiveDateTime::compare(&bytes, &bytes), + std::cmp::Ordering::Equal, + "Bytes should compare equal to themselves" + ); + let datetime_from_bytes = NaiveDateTime::from_bytes(&bytes); + assert_eq!(datetime, datetime_from_bytes); + } + #[test] + fn test_fixed_offset() { + let offset = FixedOffset::east_opt(3600).unwrap(); // +01:00 + let bytes = FixedOffset::as_bytes(&offset); + assert_eq!( + FixedOffset::fixed_width(), + Some(bytes.len()), + "FixedOffset should have fixed width" + ); + assert_eq!( + FixedOffset::compare(&bytes, &bytes), + std::cmp::Ordering::Equal, + "Bytes should compare equal to themselves" + ); + let offset_from_bytes = FixedOffset::from_bytes(&bytes); + assert_eq!(offset, offset_from_bytes); + } + #[test] + fn test_date_time_fixed_offset() { + let date = NaiveDate::from_ymd_opt(2023, 10, 5).unwrap(); + let time = NaiveTime::from_hms_opt(12, 30, 45).unwrap(); + let naive_datetime = NaiveDateTime::new(date, time); + for i in -12..=12 { + let offset = FixedOffset::east_opt(i * 3600).unwrap(); // i hours offset + let datetime = offset.from_utc_datetime(&naive_datetime); + let bytes = DateTime::::as_bytes(&datetime); + assert_eq!( + DateTime::::fixed_width(), + Some(bytes.len()), + "DateTime should have fixed width" + ); + assert_eq!( + DateTime::::compare(&bytes, &bytes), + std::cmp::Ordering::Equal, + "Bytes should compare equal to themselves" + ); + let datetime_from_bytes = DateTime::::from_bytes(&bytes); + assert_eq!(datetime, datetime_from_bytes); + } + } + #[test] + fn test_fixed_offset_table() { + let db = Database::create(NamedTempFile::new().unwrap()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(FIXED_OFFSET_TABLE).unwrap(); + let offset = FixedOffset::east_opt(3600).unwrap(); // +01:00 + table.insert(offset, 1).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + { + let table = read_txn.open_table(FIXED_OFFSET_TABLE).unwrap(); + let offset = FixedOffset::east_opt(3600).unwrap(); // +01:00 + let value = table.get(&offset).unwrap().unwrap(); + assert_eq!(value.value(), 1); + } + } + #[test] + fn test_naive_date_table() { + let db = Database::create(NamedTempFile::new().unwrap()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(NAIVE_DATE_TABLE).unwrap(); + let date = NaiveDate::from_ymd_opt(2023, 10, 5).unwrap(); + table.insert(date, 1).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + { + let table = read_txn.open_table(NAIVE_DATE_TABLE).unwrap(); + let date = NaiveDate::from_ymd_opt(2023, 10, 5).unwrap(); + let value = table.get(&date).unwrap().unwrap(); + assert_eq!(value.value(), 1); + } + } + #[test] + fn test_naive_time_table() { + let db = Database::create(NamedTempFile::new().unwrap()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(NAIVE_TIME_TABLE).unwrap(); + let time = NaiveTime::from_hms_opt(12, 30, 45).unwrap(); + table.insert(time, 1).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + { + let table = read_txn.open_table(NAIVE_TIME_TABLE).unwrap(); + let time = NaiveTime::from_hms_opt(12, 30, 45).unwrap(); + let value = table.get(&time).unwrap().unwrap(); + assert_eq!(value.value(), 1); + } + } + + #[test] + fn test_naive_datetime_table() { + let db = Database::create(NamedTempFile::new().unwrap()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(NAIVE_DATETIME_TABLE).unwrap(); + let date = NaiveDate::from_ymd_opt(2023, 10, 5).unwrap(); + let time = NaiveTime::from_hms_opt(12, 30, 45).unwrap(); + let datetime = NaiveDateTime::new(date, time); + table.insert(datetime, 1).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + { + let table = read_txn.open_table(NAIVE_DATETIME_TABLE).unwrap(); + let date = NaiveDate::from_ymd_opt(2023, 10, 5).unwrap(); + let time = NaiveTime::from_hms_opt(12, 30, 45).unwrap(); + let datetime = NaiveDateTime::new(date, time); + let value = table.get(&datetime).unwrap().unwrap(); + assert_eq!(value.value(), 1); + } + } + #[test] + fn test_datetime_fixed_offset_table() { + let now = Local::now().fixed_offset(); + let db = Database::create(NamedTempFile::new().unwrap()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(DATETIME_FIXED_OFFSET_TABLE).unwrap(); + table.insert(now, 1).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + { + let table = read_txn.open_table(DATETIME_FIXED_OFFSET_TABLE).unwrap(); + let value = table.get(&now).unwrap().unwrap(); + assert_eq!(value.value(), 1); + } + } +} diff --git a/v2/redb/src/types/uuid.rs b/v2/redb/src/types/uuid.rs new file mode 100644 index 000000000..81851adf6 --- /dev/null +++ b/v2/redb/src/types/uuid.rs @@ -0,0 +1,83 @@ +use crate::{Key, TypeName, Value}; +use std::cmp::Ordering; +use uuid::Uuid; + +impl Value for Uuid { + type SelfType<'a> + = Uuid + where + Self: 'a; + type AsBytes<'a> + = &'a uuid::Bytes + where + Self: 'a; + + fn fixed_width() -> Option { + Some(16) + } + + fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a> + where + Self: 'a, + { + Uuid::from_slice(data).unwrap() + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a> + where + Self: 'b, + { + value.as_bytes() + } + + fn type_name() -> TypeName { + TypeName::new("uuid::Uuid") + } +} + +impl Key for Uuid { + fn compare(data1: &[u8], data2: &[u8]) -> Ordering { + data1.cmp(data2) + } +} + +#[cfg(test)] +mod tests { + use crate::{Database, Key, ReadableDatabase, TableDefinition, Value}; + use tempfile::NamedTempFile; + use uuid::Uuid; + + const UUID_TABLE: TableDefinition = TableDefinition::new("table"); + + #[test] + fn test_uuid_ordering() { + let uuid1 = Uuid::new_v4(); + let uuid2 = Uuid::new_v4(); + let bytes1 = ::as_bytes(&uuid1); + let bytes2 = ::as_bytes(&uuid2); + assert_eq!( + uuid1.cmp(&uuid2), + Uuid::compare(bytes1.as_slice(), bytes2.as_slice()) + ); + } + + #[test] + fn test_uuid_table() { + let uuid1 = Uuid::new_v4(); + let uuid2 = Uuid::new_v4(); + let db = Database::create(NamedTempFile::new().unwrap()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(UUID_TABLE).unwrap(); + table.insert(uuid1, uuid2).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + { + let table = read_txn.open_table(UUID_TABLE).unwrap(); + let value = table.get(&uuid1).unwrap().unwrap(); + assert_eq!(value.value(), uuid2); + } + } +} diff --git a/v2/redb/tests/backward_compatibility.rs b/v2/redb/tests/backward_compatibility.rs new file mode 100644 index 000000000..c61357e50 --- /dev/null +++ b/v2/redb/tests/backward_compatibility.rs @@ -0,0 +1,339 @@ +use redb::{ReadableDatabase, ReadableTableMetadata}; + +const ELEMENTS: usize = 3; + +trait TestData: redb::Value + redb2_6::Value { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] + where + Self: 'a; + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] + where + Self: 'a; +} + +impl TestData for u8 { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] { + [0, 1, 2] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] { + [0, 1, 2] + } +} + +impl TestData for u16 { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] { + [0, 1, 2] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] { + [0, 1, 2] + } +} + +impl TestData for u32 { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] { + [0, 1, 2] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] { + [0, 1, 2] + } +} + +impl TestData for u64 { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] { + [0, 1, 2] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] { + [0, 1, 2] + } +} + +impl TestData for u128 { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] { + [0, 1, 2] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] { + [0, 1, 2] + } +} + +impl TestData for i8 { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] { + [-1, 1, 2] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] { + [-1, 1, 2] + } +} + +impl TestData for i16 { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] { + [-1, 1, 2] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] { + [-1, 1, 2] + } +} + +impl TestData for i32 { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] { + [-1, 1, 2] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] { + [-1, 1, 2] + } +} + +impl TestData for i64 { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] { + [-1, 1, 2] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] { + [-1, 1, 2] + } +} + +impl TestData for i128 { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] { + [-1, 1, 2] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] { + [-1, 1, 2] + } +} + +impl TestData for f32 { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] { + [f32::NAN, f32::INFINITY, f32::MIN_POSITIVE] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] { + [f32::NAN, f32::INFINITY, f32::MIN_POSITIVE] + } +} + +impl TestData for f64 { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] { + [f64::MIN, f64::NEG_INFINITY, f64::MAX] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] { + [f64::MIN, f64::NEG_INFINITY, f64::MAX] + } +} + +impl TestData for () { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] { + [(), (), ()] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] { + [(), (), ()] + } +} + +impl TestData for &'static str { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] { + ["hello", "world1", "hi"] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] { + ["hello", "world1", "hi"] + } +} + +impl TestData for &'static [u8] { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] { + [b"test", b"bytes", b"now"] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] { + [b"test", b"bytes", b"now"] + } +} + +impl TestData for &'static [u8; 5] { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] { + [b"test1", b"bytes", b"now12"] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] { + [b"test1", b"bytes", b"now12"] + } +} + +impl TestData for [&str; 3] { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] + where + Self: 'a, + { + [ + ["test1", "hi", "world"], + ["test2", "hi", "world"], + ["test3", "hi", "world"], + ] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] + where + Self: 'a, + { + [ + ["test1", "hi", "world"], + ["test2", "hi", "world"], + ["test3", "hi", "world"], + ] + } +} + +impl TestData for [u128; 3] { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] { + [[1, 2, 3], [3, 2, 1], [300, 200, 100]] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] { + [[1, 2, 3], [3, 2, 1], [300, 200, 100]] + } +} + +impl TestData for Vec<&str> { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] + where + Self: 'a, + { + [ + vec!["test1", "hi", "world"], + vec!["test2", "hi", "world"], + vec!["test3", "hi", "world"], + ] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] + where + Self: 'a, + { + [ + vec!["test1", "hi", "world"], + vec!["test2", "hi", "world"], + vec!["test3", "hi", "world"], + ] + } +} + +impl TestData for Option { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] { + [None, Some(0), Some(7)] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] { + [None, Some(0), Some(7)] + } +} + +impl TestData for (u64, &'static str) { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] { + [(0, "hi"), (1, "bye"), (2, "byte")] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] { + [(0, "hi"), (1, "bye"), (2, "byte")] + } +} + +impl TestData for (u64, u32) { + fn make_data_v2_6<'a>() -> [::SelfType<'a>; ELEMENTS] { + [(0, 3), (1, 4), (2, 5)] + } + + fn make_data<'a>() -> [::SelfType<'a>; ELEMENTS] { + [(0, 3), (1, 4), (2, 5)] + } +} + +fn create_tempfile() -> tempfile::NamedTempFile { + if cfg!(target_os = "wasi") { + tempfile::NamedTempFile::new_in("/tmp").unwrap() + } else { + tempfile::NamedTempFile::new().unwrap() + } +} + +fn test_helper() { + { + let tmpfile = create_tempfile(); + let db = redb2_6::Database::builder() + .create_with_file_format_v3(true) + .create(tmpfile.path()) + .unwrap(); + let table_def: redb2_6::TableDefinition = redb2_6::TableDefinition::new("table"); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(table_def).unwrap(); + for i in 0..ELEMENTS { + table + .insert(&K::make_data_v2_6()[i], &V::make_data_v2_6()[i]) + .unwrap(); + } + } + write_txn.commit().unwrap(); + drop(db); + + let db = redb::Database::open(tmpfile.path()).unwrap(); + let read_txn = db.begin_read().unwrap(); + let table_def: redb::TableDefinition = redb::TableDefinition::new("table"); + let table = read_txn.open_table(table_def).unwrap(); + assert_eq!(table.len().unwrap(), ELEMENTS as u64); + for i in 0..ELEMENTS { + let result = table.get(&K::make_data()[i]).unwrap().unwrap(); + let value = result.value(); + let bytes = ::as_bytes(&value); + let expected = &V::make_data()[i]; + let expected_bytes = ::as_bytes(expected); + assert_eq!(bytes.as_ref(), expected_bytes.as_ref()); + } + } +} + +#[test] +fn primitive_types() { + test_helper::(); + test_helper::(); + test_helper::(); + test_helper::(); + test_helper::(); + test_helper::(); + test_helper::(); + test_helper::(); + test_helper::(); + test_helper::(); + test_helper::(); + test_helper::(); + test_helper::<&str, &str>(); + test_helper::(); +} + +#[test] +fn container_types() { + test_helper::<&[u8], &[u8]>(); + test_helper::<&[u8; 5], &[u8; 5]>(); + test_helper::>(); + test_helper::<(u64, u32), &str>(); + test_helper::<[&str; 3], [u128; 3]>(); + test_helper::>(); +} + +#[test] +fn mixed_width() { + test_helper::(); + test_helper::<&[u8; 5], &str>(); +} diff --git a/v2/redb/tests/basic_tests.rs b/v2/redb/tests/basic_tests.rs new file mode 100644 index 000000000..4bd0bf739 --- /dev/null +++ b/v2/redb/tests/basic_tests.rs @@ -0,0 +1,1962 @@ +use rand::random; +#[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] +use redb::DatabaseError; +use redb::backends::InMemoryBackend; +use redb::{ + Database, Key, MultimapTableDefinition, MultimapTableHandle, Range, ReadOnlyDatabase, + ReadableDatabase, ReadableTable, ReadableTableMetadata, TableDefinition, TableError, + TableHandle, TypeName, Value, +}; +use std::cmp::Ordering; +#[cfg(not(target_os = "wasi"))] +use std::sync; + +const SLICE_TABLE: TableDefinition<&[u8], &[u8]> = TableDefinition::new("slice"); +const STR_TABLE: TableDefinition<&str, &str> = TableDefinition::new("x"); +const U64_TABLE: TableDefinition = TableDefinition::new("u64"); + +fn create_tempfile() -> tempfile::NamedTempFile { + if cfg!(target_os = "wasi") { + tempfile::NamedTempFile::new_in("/tmp").unwrap() + } else { + tempfile::NamedTempFile::new().unwrap() + } +} + +#[test] +fn len() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + table.insert("hello", "world").unwrap(); + table.insert("hello2", "world2").unwrap(); + table.insert("hi", "world").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(STR_TABLE).unwrap(); + assert_eq!(table.len().unwrap(), 3); + let untyped_table = read_txn.open_untyped_table(STR_TABLE).unwrap(); + assert_eq!(untyped_table.len().unwrap(), 3); +} + +#[test] +fn read_only() { + let tmpfile = create_tempfile(); + { + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + table.insert("hello", "world").unwrap(); + table.insert("hello2", "world2").unwrap(); + table.insert("hi", "world").unwrap(); + } + write_txn.commit().unwrap(); + + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] + assert!(matches!( + ReadOnlyDatabase::open(tmpfile.path()), + Err(DatabaseError::DatabaseAlreadyOpen) + )); + drop(db); + } + + let db = ReadOnlyDatabase::open(tmpfile.path()).unwrap(); + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(STR_TABLE).unwrap(); + assert_eq!(table.len().unwrap(), 3); + + let db2 = ReadOnlyDatabase::open(tmpfile.path()).unwrap(); + let read_txn2 = db.begin_read().unwrap(); + let table2 = read_txn2.open_table(STR_TABLE).unwrap(); + assert_eq!(table2.len().unwrap(), 3); + + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] + assert!(matches!( + Database::open(tmpfile.path()), + Err(DatabaseError::DatabaseAlreadyOpen) + )); + drop(db); + drop(db2); +} + +#[test] +fn table_stats() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + table.insert("hello", "world").unwrap(); + table.insert("hello2", "world2").unwrap(); + table.insert("hi", "world").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(STR_TABLE).unwrap(); + let untyped_table = read_txn.open_untyped_table(STR_TABLE).unwrap(); + assert_eq!(table.stats().unwrap().tree_height(), 1); + assert_eq!(untyped_table.stats().unwrap().tree_height(), 1); +} + +#[test] +fn in_memory() { + let db = Database::builder() + .create_with_backend(InMemoryBackend::new()) + .unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + table.insert("hello", "world").unwrap(); + table.insert("hello2", "world2").unwrap(); + table.insert("hi", "world").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(STR_TABLE).unwrap(); + assert_eq!(table.len().unwrap(), 3); +} + +#[test] +fn first_last() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + assert!(table.first().unwrap().is_none()); + assert!(table.last().unwrap().is_none()); + table.insert("a", "world1").unwrap(); + assert_eq!(table.first().unwrap().unwrap().0.value(), "a"); + assert_eq!(table.last().unwrap().unwrap().0.value(), "a"); + table.insert("b", "world2").unwrap(); + table.insert("c", "world3").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(STR_TABLE).unwrap(); + assert_eq!(table.first().unwrap().unwrap().0.value(), "a"); + assert_eq!(table.last().unwrap().unwrap().0.value(), "c"); +} + +#[test] +fn pop() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + + assert!(table.pop_first().unwrap().is_none()); + assert!(table.pop_last().unwrap().is_none()); + + table.insert("a", "world").unwrap(); + table.insert("b", "world2").unwrap(); + table.insert("c", "world3").unwrap(); + } + write_txn.commit().unwrap(); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + { + let (key, value) = table.pop_first().unwrap().unwrap(); + assert_eq!(key.value(), "a"); + assert_eq!(value.value(), "world"); + } + { + let (key, value) = table.pop_last().unwrap().unwrap(); + assert_eq!(key.value(), "c"); + assert_eq!(value.value(), "world3"); + } + { + let (key, value) = table.pop_last().unwrap().unwrap(); + assert_eq!(key.value(), "b"); + assert_eq!(value.value(), "world2"); + } + + assert!(table.pop_first().unwrap().is_none()); + assert!(table.pop_last().unwrap().is_none()); + } + write_txn.commit().unwrap(); +} + +#[test] +fn extract_if() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(U64_TABLE).unwrap(); + for i in 0..10 { + table.insert(&i, &i).unwrap(); + } + // Test retain uncommitted data + let mut extracted = table.extract_if(|k, _| k >= 5).unwrap(); + assert_eq!(extracted.next().unwrap().unwrap().0.value(), 5); + drop(extracted); + assert_eq!(table.len().unwrap(), 9); + + let mut extracted = table.extract_from_if(5.., |k, _| k < 8).unwrap(); + assert_eq!(extracted.next().unwrap().unwrap().0.value(), 6); + assert_eq!(extracted.next().unwrap().unwrap().0.value(), 7); + assert!(extracted.next().is_none()); + drop(extracted); + assert_eq!(table.len().unwrap(), 7); + + for i in 5..8 { + assert!(table.insert(&i, &i).unwrap().is_none()); + } + assert_eq!(table.len().unwrap(), 10); + } + write_txn.commit().unwrap(); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(U64_TABLE).unwrap(); + assert_eq!(table.len().unwrap(), 10); + let mut extracted = table.extract_if(|_, _| true).unwrap(); + assert_eq!(extracted.next().unwrap().unwrap().1.value(), 0); + drop(extracted); + assert_eq!(table.len().unwrap(), 9); + } + write_txn.abort().unwrap(); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(U64_TABLE).unwrap(); + assert_eq!(table.len().unwrap(), 10); + for _ in table.extract_if(|x, _| x % 2 != 0).unwrap() {} + table.extract_if(|_, _| true).unwrap().next_back(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_write().unwrap(); + { + let table = read_txn.open_table(U64_TABLE).unwrap(); + assert_eq!(table.len().unwrap(), 4); + let mut iter = table.iter().unwrap(); + for x in [0, 2, 4, 6] { + let (k, v) = iter.next().unwrap().unwrap(); + assert_eq!(k.value(), x); + assert_eq!(k.value(), v.value()); + } + } +} + +#[test] +fn retain() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(U64_TABLE).unwrap(); + for i in 0..10 { + table.insert(&i, &i).unwrap(); + } + // Test retain uncommitted data + table.retain(|k, _| k >= 5).unwrap(); + for i in 0..5 { + assert!(table.insert(&i, &i).unwrap().is_none()); + } + assert_eq!(table.len().unwrap(), 10); + + // Test matching on the value + table.retain(|_, v| v >= 5).unwrap(); + for i in 0..5 { + assert!(table.insert(&i, &i).unwrap().is_none()); + } + assert_eq!(table.len().unwrap(), 10); + + // Test retain_in + table.retain_in(..5, |_, _| false).unwrap(); + for i in 0..5 { + assert!(table.insert(&i, &i).unwrap().is_none()); + } + assert_eq!(table.len().unwrap(), 10); + } + write_txn.commit().unwrap(); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(U64_TABLE).unwrap(); + assert_eq!(table.len().unwrap(), 10); + table.retain(|x, _| x >= 5).unwrap(); + assert_eq!(table.len().unwrap(), 5); + + let mut i = 5u64; + for item in table.range(0..10).unwrap() { + let (k, v) = item.unwrap(); + assert_eq!(i, k.value()); + assert_eq!(i, v.value()); + i += 1; + } + } + write_txn.abort().unwrap(); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(U64_TABLE).unwrap(); + table.retain(|x, _| x % 2 == 0).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_write().unwrap(); + { + let table = read_txn.open_table(U64_TABLE).unwrap(); + assert_eq!(table.len().unwrap(), 5); + for entry in table.iter().unwrap() { + let (k, v) = entry.unwrap(); + assert_eq!(k.value() % 2, 0); + assert_eq!(k.value(), v.value()); + } + } +} + +#[test] +fn stored_size() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + table.insert("hello", "world").unwrap(); + } + write_txn.commit().unwrap(); + + let write_txn = db.begin_write().unwrap(); + assert_eq!(write_txn.stats().unwrap().stored_bytes(), 10); + assert!(write_txn.stats().unwrap().fragmented_bytes() > 0); + assert!(write_txn.stats().unwrap().metadata_bytes() > 0); + write_txn.abort().unwrap(); +} + +#[test] +fn create_open() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(U64_TABLE).unwrap(); + table.insert(&0, &1).unwrap(); + } + write_txn.commit().unwrap(); + drop(db); + + let db2 = Database::open(tmpfile.path()).unwrap(); + + let read_txn = db2.begin_read().unwrap(); + let table = read_txn.open_table(U64_TABLE).unwrap(); + assert_eq!(1, table.get(&0).unwrap().unwrap().value()); +} + +#[test] +fn multiple_tables() { + let definition1: TableDefinition<&str, &str> = TableDefinition::new("1"); + let definition2: TableDefinition<&str, &str> = TableDefinition::new("2"); + + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(definition1).unwrap(); + let mut table2 = write_txn.open_table(definition2).unwrap(); + + table.insert("hello", "world").unwrap(); + table2.insert("hello", "world2").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(definition1).unwrap(); + let table2 = read_txn.open_table(definition2).unwrap(); + assert_eq!(table.len().unwrap(), 1); + assert_eq!("world", table.get("hello").unwrap().unwrap().value()); + assert_eq!(table2.len().unwrap(), 1); + assert_eq!("world2", table2.get("hello").unwrap().unwrap().value()); +} + +#[test] +fn list_tables() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition_x: TableDefinition<&[u8], &[u8]> = TableDefinition::new("x"); + let definition_y: TableDefinition<&[u8], &[u8]> = TableDefinition::new("y"); + let definition_mx: MultimapTableDefinition<&[u8], &[u8]> = MultimapTableDefinition::new("mx"); + let definition_my: MultimapTableDefinition<&[u8], &[u8]> = MultimapTableDefinition::new("my"); + + let write_txn = db.begin_write().unwrap(); + { + write_txn.open_table(definition_x).unwrap(); + write_txn.open_table(definition_y).unwrap(); + write_txn.open_multimap_table(definition_mx).unwrap(); + write_txn.open_multimap_table(definition_my).unwrap(); + } + + let tables: Vec = write_txn + .list_tables() + .unwrap() + .map(|h| h.name().to_string()) + .collect(); + let multimap_tables: Vec = write_txn + .list_multimap_tables() + .unwrap() + .map(|h| h.name().to_string()) + .collect(); + assert_eq!(tables, &["x", "y"]); + assert_eq!(multimap_tables, &["mx", "my"]); + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let tables: Vec = read_txn + .list_tables() + .unwrap() + .map(|h| h.name().to_string()) + .collect(); + let multimap_tables: Vec = read_txn + .list_multimap_tables() + .unwrap() + .map(|h| h.name().to_string()) + .collect(); + assert_eq!(tables, &["x", "y"]); + assert_eq!(multimap_tables, &["mx", "my"]); +} + +#[test] +// Test that these signatures compile +fn tuple_type_function_lifetime() { + #[allow(dead_code)] + fn insert_inferred_lifetime(table: &mut redb::Table<(&str, u8), u64>) { + table + .insert(&(String::from("hello").as_str(), 8), &1) + .unwrap(); + } + + #[allow(dead_code)] + #[allow(clippy::needless_lifetimes)] + fn insert_explicit_lifetime<'a>(table: &mut redb::Table<(&'a str, u8), u64>) { + table + .insert(&(String::from("hello").as_str(), 8), &1) + .unwrap(); + } +} + +#[test] +fn tuple_type_lifetime() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition<(&str, u8), (u16, u32)> = TableDefinition::new("table"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(table_def).unwrap(); + table + .insert(&(String::from("hello").as_str(), 5), &(0, 123)) + .unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(table_def).unwrap(); + assert_eq!(table.get(&("hello", 5)).unwrap().unwrap().value(), (0, 123)); +} + +#[test] +fn tuple2_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition<(&str, u8), (u16, u32)> = TableDefinition::new("table"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(table_def).unwrap(); + table.insert(&("hello", 5), &(0, 123)).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(table_def).unwrap(); + assert_eq!(table.get(&("hello", 5)).unwrap().unwrap().value(), (0, 123)); +} + +#[test] +fn tuple3_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition<(&str, u8, u16), (u16, u32)> = TableDefinition::new("table"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(table_def).unwrap(); + table.insert(&("hello", 5, 6), &(0, 123)).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(table_def).unwrap(); + assert_eq!( + table.get(&("hello", 5, 6)).unwrap().unwrap().value(), + (0, 123) + ); +} + +#[test] +fn tuple4_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition<(&str, u8, u16, u32), (u16, u32)> = + TableDefinition::new("table"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(table_def).unwrap(); + table.insert(&("hello", 5, 6, 7), &(0, 123)).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(table_def).unwrap(); + assert_eq!( + table.get(&("hello", 5, 6, 7)).unwrap().unwrap().value(), + (0, 123) + ); +} + +#[test] +#[allow(clippy::type_complexity)] +fn tuple5_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition<(&str, u8, u16, u32, u64), (u16, u32)> = + TableDefinition::new("table"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(table_def).unwrap(); + table.insert(&("hello", 5, 6, 7, 8), &(0, 123)).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(table_def).unwrap(); + assert_eq!( + table.get(&("hello", 5, 6, 7, 8)).unwrap().unwrap().value(), + (0, 123) + ); +} + +#[test] +#[allow(clippy::type_complexity)] +fn tuple6_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition<(&str, u8, u16, u32, u64, u128), (u16, u32)> = + TableDefinition::new("table"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(table_def).unwrap(); + table.insert(&("hello", 5, 6, 7, 8, 9), &(0, 123)).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(table_def).unwrap(); + assert_eq!( + table + .get(&("hello", 5, 6, 7, 8, 9)) + .unwrap() + .unwrap() + .value(), + (0, 123) + ); +} + +#[test] +#[allow(clippy::type_complexity)] +fn tuple7_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition<(&str, u8, u16, u32, u64, u128, i8), (u16, u32)> = + TableDefinition::new("table"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(table_def).unwrap(); + table + .insert(&("hello", 5, 6, 7, 8, 9, -1), &(0, 123)) + .unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(table_def).unwrap(); + assert_eq!( + table + .get(&("hello", 5, 6, 7, 8, 9, -1)) + .unwrap() + .unwrap() + .value(), + (0, 123) + ); +} + +#[test] +#[allow(clippy::type_complexity)] +fn tuple8_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition<(&str, u8, u16, u32, u64, u128, i8, i16), (u16, u32)> = + TableDefinition::new("table"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(table_def).unwrap(); + table + .insert(&("hello", 5, 6, 7, 8, 9, -1, -2), &(0, 123)) + .unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(table_def).unwrap(); + assert_eq!( + table + .get(&("hello", 5, 6, 7, 8, 9, -1, -2)) + .unwrap() + .unwrap() + .value(), + (0, 123) + ); +} + +#[test] +#[allow(clippy::type_complexity)] +fn tuple9_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition<(&str, u8, u16, u32, u64, u128, i8, i16, i32), (u16, u32)> = + TableDefinition::new("table"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(table_def).unwrap(); + table + .insert(&("hello", 5, 6, 7, 8, 9, -1, -2, -3), &(0, 123)) + .unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(table_def).unwrap(); + assert_eq!( + table + .get(&("hello", 5, 6, 7, 8, 9, -1, -2, -3)) + .unwrap() + .unwrap() + .value(), + (0, 123) + ); +} + +#[test] +#[allow(clippy::type_complexity)] +fn tuple10_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition<(&str, u8, u16, u32, u64, u128, i8, i16, i32, i64), (u16, u32)> = + TableDefinition::new("table"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(table_def).unwrap(); + table + .insert(&("hello", 5, 6, 7, 8, 9, -1, -2, -3, -4), &(0, 123)) + .unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(table_def).unwrap(); + assert_eq!( + table + .get(&("hello", 5, 6, 7, 8, 9, -1, -2, -3, -4)) + .unwrap() + .unwrap() + .value(), + (0, 123) + ); +} + +#[test] +#[allow(clippy::type_complexity)] +fn tuple11_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition< + (&str, u8, u16, u32, u64, u128, i8, i16, i32, i64, i128), + (u16, u32), + > = TableDefinition::new("table"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(table_def).unwrap(); + table + .insert(&("hello", 5, 6, 7, 8, 9, -1, -2, -3, -4, -5), &(0, 123)) + .unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(table_def).unwrap(); + assert_eq!( + table + .get(&("hello", 5, 6, 7, 8, 9, -1, -2, -3, -4, -5)) + .unwrap() + .unwrap() + .value(), + (0, 123) + ); +} + +#[test] +#[allow(clippy::type_complexity)] +fn tuple12_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition< + ( + &str, + u8, + u16, + u32, + u64, + u128, + &str, + i16, + i32, + i64, + i128, + &str, + ), + (u16, u32), + > = TableDefinition::new("table"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(table_def).unwrap(); + table + .insert( + &("hello", 5, 6, 7, 8, 9, "mid", -2, -3, -4, -5, "end"), + &(0, 123), + ) + .unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(table_def).unwrap(); + assert_eq!( + table + .get(&("hello", 5, 6, 7, 8, 9, "mid", -2, -3, -4, -5, "end")) + .unwrap() + .unwrap() + .value(), + (0, 123) + ); +} + +#[test] +#[allow(clippy::type_complexity)] +fn generic_array_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def1: TableDefinition<[u8; 3], [u64; 2]> = TableDefinition::new("table1"); + let table_def2: TableDefinition<[(u8, &str); 2], [Option<&str>; 2]> = + TableDefinition::new("table2"); + let table_def3: TableDefinition<[&[u8]; 2], [f32; 2]> = TableDefinition::new("table3"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table1 = write_txn.open_table(table_def1).unwrap(); + let mut table2 = write_txn.open_table(table_def2).unwrap(); + let mut table3 = write_txn.open_table(table_def3).unwrap(); + table1.insert([0, 1, 2], &[4, 5]).unwrap(); + table2 + .insert([(0, "hi"), (1, "world")], [None, Some("test")]) + .unwrap(); + table3 + .insert([b"hi".as_slice(), b"world".as_slice()], [4.0, 5.0]) + .unwrap(); + table3 + .insert([b"longlong".as_slice(), b"longlong".as_slice()], [0.0, 0.0]) + .unwrap(); + table3 + .insert([b"s".as_slice(), b"s".as_slice()], [0.0, 0.0]) + .unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table1 = read_txn.open_table(table_def1).unwrap(); + let table2 = read_txn.open_table(table_def2).unwrap(); + let table3 = read_txn.open_table(table_def3).unwrap(); + assert_eq!(table1.get(&[0, 1, 2]).unwrap().unwrap().value(), [4, 5]); + assert_eq!( + table2 + .get(&[(0, "hi"), (1, "world")]) + .unwrap() + .unwrap() + .value(), + [None, Some("test")] + ); + assert_eq!( + table3 + .get(&[b"hi".as_slice(), b"world".as_slice()]) + .unwrap() + .unwrap() + .value(), + [4.0, 5.0] + ); +} + +#[test] +fn is_empty() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + assert!(table.is_empty().unwrap()); + table.insert("hello", "world").unwrap(); + assert!(!table.is_empty().unwrap()); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(STR_TABLE).unwrap(); + assert!(!table.is_empty().unwrap()); +} + +#[test] +fn abort() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + table.insert("hello", "aborted").unwrap(); + assert_eq!("aborted", table.get("hello").unwrap().unwrap().value()); + } + write_txn.abort().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(STR_TABLE); + assert!(table.is_err()); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + table.insert("hello", "world").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(STR_TABLE).unwrap(); + assert_eq!("world", table.get("hello").unwrap().unwrap().value()); + assert_eq!(table.len().unwrap(), 1); +} + +#[test] +fn insert_overwrite() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + assert!(table.insert("hello", "world").unwrap().is_none()); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(STR_TABLE).unwrap(); + assert_eq!("world", table.get("hello").unwrap().unwrap().value()); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + let old_value = table.insert("hello", "replaced").unwrap(); + assert_eq!(old_value.unwrap().value(), "world"); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(STR_TABLE).unwrap(); + assert_eq!("replaced", table.get("hello").unwrap().unwrap().value()); +} + +#[test] +fn insert_reserve() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let def: TableDefinition<&str, &[u8]> = TableDefinition::new("x"); + let value = "world"; + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(def).unwrap(); + let mut reserved = table.insert_reserve("hello", value.len()).unwrap(); + reserved.as_mut().copy_from_slice(value.as_bytes()); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(def).unwrap(); + assert_eq!( + value.as_bytes(), + table.get("hello").unwrap().unwrap().value() + ); +} + +#[test] +fn get_mut() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + assert!(table.insert("hello", "world").unwrap().is_none()); + } + write_txn.commit().unwrap(); + + { + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(STR_TABLE).unwrap(); + assert_eq!("world", table.get("hello").unwrap().unwrap().value()); + } + + let mut very_long_string = String::from("hello"); + for _ in 0..10_000 { + very_long_string.push('x'); + } + + let mut last_value = "world"; + + for new_value in ["earth", "mars", very_long_string.as_str()].iter() { + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + let mut value = table.get_mut("hello").unwrap().unwrap(); + if value.value() == last_value { + value.insert(new_value).unwrap(); + } else { + panic!(); + } + assert_eq!(value.value(), *new_value); + last_value = new_value; + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(STR_TABLE).unwrap(); + assert_eq!(*new_value, table.get("hello").unwrap().unwrap().value()); + } +} + +#[test] +fn delete() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + table.insert("hello", "world").unwrap(); + table.insert("hello2", "world").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(STR_TABLE).unwrap(); + assert_eq!("world", table.get("hello").unwrap().unwrap().value()); + assert_eq!(table.len().unwrap(), 2); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + assert_eq!("world", table.remove("hello").unwrap().unwrap().value()); + assert!(table.remove("hello").unwrap().is_none()); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(STR_TABLE).unwrap(); + assert!(table.get("hello").unwrap().is_none()); + assert_eq!(table.len().unwrap(), 1); +} + +#[test] +fn delete_open_table() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let table = write_txn.open_table(STR_TABLE).unwrap(); + assert!(matches!( + write_txn.delete_table(STR_TABLE).unwrap_err(), + TableError::TableAlreadyOpen(_, _) + )); + drop(table); + } + write_txn.commit().unwrap(); +} + +#[test] +fn delete_table() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let table = write_txn.open_table(STR_TABLE).unwrap(); + assert!(write_txn.delete_table(table).unwrap()); + } + write_txn.commit().unwrap(); +} + +#[test] +fn rename_table() { + let table_def: TableDefinition<&str, &str> = TableDefinition::new("x"); + let table_def2: TableDefinition<&str, &str> = TableDefinition::new("x2"); + let multitable_def: MultimapTableDefinition<&str, &str> = MultimapTableDefinition::new("x"); + let multitable_def2: MultimapTableDefinition<&str, &str> = MultimapTableDefinition::new("x2"); + + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(table_def).unwrap(); + table.insert("hi", "hi").unwrap(); + write_txn.rename_table(table, table_def2).unwrap(); + assert!(matches!( + write_txn.rename_table(table_def, table_def2).unwrap_err(), + TableError::TableDoesNotExist(_) + )); + + let table = write_txn.open_table(table_def).unwrap(); + assert!(matches!( + write_txn.rename_table(table, table_def2).unwrap_err(), + TableError::TableExists(_) + )); + + assert!(matches!( + write_txn + .rename_multimap_table(multitable_def, multitable_def2) + .unwrap_err(), + TableError::TableIsNotMultimap(_) + )); + } + write_txn.commit().unwrap(); + + let write_txn = db.begin_write().unwrap(); + { + let table = write_txn.open_table(table_def).unwrap(); + assert!(table.is_empty().unwrap()); + let table2 = write_txn.open_table(table_def2).unwrap(); + assert_eq!(table2.get("hi").unwrap().unwrap().value(), "hi"); + } +} + +#[test] +fn rename_open_table() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let table = write_txn.open_table(STR_TABLE).unwrap(); + assert!(matches!( + write_txn.rename_table(STR_TABLE, STR_TABLE).unwrap_err(), + TableError::TableAlreadyOpen(_, _) + )); + drop(table); + } + write_txn.commit().unwrap(); +} + +#[test] +fn no_dirty_reads() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + table.insert("hello", "world").unwrap(); + } + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(STR_TABLE); + assert!(table.is_err()); + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(STR_TABLE).unwrap(); + assert_eq!("world", table.get("hello").unwrap().unwrap().value()); +} + +#[test] +fn read_isolation() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + table.insert("hello", "world").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(STR_TABLE).unwrap(); + assert_eq!("world", table.get("hello").unwrap().unwrap().value()); + + let write_txn = db.begin_write().unwrap(); + { + let mut write_table = write_txn.open_table(STR_TABLE).unwrap(); + write_table.remove("hello").unwrap(); + write_table.insert("hello2", "world2").unwrap(); + write_table.insert("hello3", "world3").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn2 = db.begin_read().unwrap(); + let table2 = read_txn2.open_table(STR_TABLE).unwrap(); + assert!(table2.get("hello").unwrap().is_none()); + assert_eq!("world2", table2.get("hello2").unwrap().unwrap().value()); + assert_eq!("world3", table2.get("hello3").unwrap().unwrap().value()); + assert_eq!(table2.len().unwrap(), 2); + + assert_eq!("world", table.get("hello").unwrap().unwrap().value()); + assert!(table.get("hello2").unwrap().is_none()); + assert!(table.get("hello3").unwrap().is_none()); + assert_eq!(table.len().unwrap(), 1); +} + +#[test] +fn read_isolation2() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + table.insert("hello", "world").unwrap(); + } + write_txn.commit().unwrap(); + + let write_txn = db.begin_write().unwrap(); + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(STR_TABLE).unwrap(); + assert_eq!("world", table.get("hello").unwrap().unwrap().value()); + { + let mut write_table = write_txn.open_table(STR_TABLE).unwrap(); + write_table.remove("hello").unwrap(); + write_table.insert("hello2", "world2").unwrap(); + write_table.insert("hello3", "world3").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn2 = db.begin_read().unwrap(); + let table2 = read_txn2.open_table(STR_TABLE).unwrap(); + assert!(table2.get("hello").unwrap().is_none()); + assert_eq!("world2", table2.get("hello2").unwrap().unwrap().value()); + assert_eq!("world3", table2.get("hello3").unwrap().unwrap().value()); + assert_eq!(table2.len().unwrap(), 2); + + assert_eq!("world", table.get("hello").unwrap().unwrap().value()); + assert!(table.get("hello2").unwrap().is_none()); + assert!(table.get("hello3").unwrap().is_none()); + assert_eq!(table.len().unwrap(), 1); +} + +#[test] +fn reopen_table() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(U64_TABLE).unwrap(); + table.insert(&0, &0).unwrap(); + } + { + let mut table = write_txn.open_table(U64_TABLE).unwrap(); + table.insert(&1, &1).unwrap(); + } + write_txn.commit().unwrap(); +} + +#[test] +fn u64_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(U64_TABLE).unwrap(); + table.insert(&0, &1).unwrap(); + table.insert(&1, &1).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(U64_TABLE).unwrap(); + assert_eq!( + 2u64, + table + .range(0..2) + .unwrap() + .map(|item| item.unwrap().1.value()) + .sum::() + ); + assert_eq!(1, table.get(&0).unwrap().unwrap().value()); +} + +#[test] +fn i128_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + + let definition: TableDefinition = TableDefinition::new("x"); + + { + let mut table = write_txn.open_table(definition).unwrap(); + for i in -10..=10 { + table.insert(&i, &(i - 1)).unwrap(); + } + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(definition).unwrap(); + assert_eq!(-2, table.get(&-1).unwrap().unwrap().value()); + let mut iter: Range = table.range::(..).unwrap(); + for i in -11..10 { + assert_eq!(iter.next().unwrap().unwrap().1.value(), i); + } + assert!(iter.next().is_none()); +} + +#[test] +fn f32_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: TableDefinition = TableDefinition::new("x"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(definition).unwrap(); + table.insert(&0, &0.3).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(definition).unwrap(); + assert_eq!(0.3, table.get(&0).unwrap().unwrap().value()); +} + +#[test] +fn str_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: TableDefinition<&str, &str> = TableDefinition::new("x"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(definition).unwrap(); + table.insert("hello", "world").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(definition).unwrap(); + assert_eq!("world", table.get("hello").unwrap().unwrap().value()); + + let mut iter = table.iter().unwrap(); + assert_eq!(iter.next().unwrap().unwrap().1.value(), "world"); + assert!(iter.next().is_none()); + + let mut iter: Range<&str, &str> = table.range("a".."z").unwrap(); + assert_eq!(iter.next().unwrap().unwrap().1.value(), "world"); + assert!(iter.next().is_none()); +} + +#[test] +fn string_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: TableDefinition = TableDefinition::new("x"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(definition).unwrap(); + table + .insert("hello".to_string(), "world".to_string()) + .unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(definition).unwrap(); + assert_eq!( + "world", + table.get("hello".to_string()).unwrap().unwrap().value() + ); + + let mut iter = table.iter().unwrap(); + assert_eq!(iter.next().unwrap().unwrap().1.value(), "world"); + assert!(iter.next().is_none()); + + let mut iter: Range = table.range("a".to_string().."z".to_string()).unwrap(); + assert_eq!(iter.next().unwrap().unwrap().1.value(), "world"); + assert!(iter.next().is_none()); +} + +#[test] +fn empty_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: TableDefinition = TableDefinition::new("x"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(definition).unwrap(); + table.insert(&0, &()).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(definition).unwrap(); + assert!(!table.is_empty().unwrap()); +} + +#[test] +#[allow(clippy::bool_assert_comparison)] +fn bool_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: TableDefinition = TableDefinition::new("x"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(definition).unwrap(); + table.insert(true, &false).unwrap(); + table.insert(&false, false).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(definition).unwrap(); + assert_eq!(false, table.get(&true).unwrap().unwrap().value()); + assert_eq!(false, table.get(&false).unwrap().unwrap().value()); + + let mut iter = table.iter().unwrap(); + assert_eq!(iter.next().unwrap().unwrap().0.value(), false); + assert_eq!(iter.next().unwrap().unwrap().0.value(), true); + assert!(iter.next().is_none()); +} + +#[test] +fn option_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: TableDefinition, Option> = TableDefinition::new("x"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(definition).unwrap(); + table.insert(None, None).unwrap(); + table.insert(None, Some(0)).unwrap(); + table.insert(Some(1), Some(1)).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(definition).unwrap(); + assert_eq!(table.get(None).unwrap().unwrap().value(), Some(0)); + assert_eq!(table.get(Some(1)).unwrap().unwrap().value(), Some(1)); + let mut iter = table.iter().unwrap(); + assert_eq!(iter.next().unwrap().unwrap().0.value(), None); + assert_eq!(iter.next().unwrap().unwrap().0.value(), Some(1)); +} + +#[test] +fn array_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: TableDefinition<&[u8; 5], &[u8; 9]> = TableDefinition::new("x"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(definition).unwrap(); + table.insert(b"hello", b"world_123").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(definition).unwrap(); + let hello = b"hello"; + assert_eq!(b"world_123", table.get(hello).unwrap().unwrap().value()); + + let mut iter: Range<&[u8; 5], &[u8; 9]> = table.range::<&[u8; 5]>(..).unwrap(); + assert_eq!(iter.next().unwrap().unwrap().1.value(), b"world_123"); + assert!(iter.next().is_none()); +} + +#[test] +fn vec_fixed_width_value_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: TableDefinition> = TableDefinition::new("x"); + + let value = vec![0, 1, 2, 3]; + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(definition).unwrap(); + table.insert(0, &value).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(definition).unwrap(); + assert_eq!(value, table.get(0).unwrap().unwrap().value()); +} + +#[test] +fn vec_var_width_value_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: TableDefinition> = TableDefinition::new("x"); + + let value = vec!["hello", "world"]; + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(definition).unwrap(); + table.insert(0, &value).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(definition).unwrap(); + assert_eq!(value, table.get(0).unwrap().unwrap().value()); +} + +#[test] +fn vec_vec_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: TableDefinition>> = TableDefinition::new("x"); + + let value = vec![vec!["hello", "world"], vec!["this", "is", "a", "test"]]; + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(definition).unwrap(); + table.insert(0, &value).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(definition).unwrap(); + assert_eq!(value, table.get(0).unwrap().unwrap().value()); +} + +#[test] +fn range_lifetime() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: TableDefinition<&str, &str> = TableDefinition::new("x"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(definition).unwrap(); + table.insert("hello", "world").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(definition).unwrap(); + + let mut iter = { + let start = "hello".to_string(); + table.range::<&str>(start.as_str()..).unwrap() + }; + assert_eq!(iter.next().unwrap().unwrap().1.value(), "world"); + assert!(iter.next().is_none()); +} + +#[test] +fn range_empty() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: TableDefinition = TableDefinition::new("x"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(definition).unwrap(); + for i in 0..1000 { + table.insert(i, i).unwrap(); + } + #[expect(clippy::reversed_empty_ranges)] + let mut iter = table.range(500..0).unwrap(); + assert!(iter.next().is_none()); + } + write_txn.commit().unwrap(); +} + +#[test] +fn extract_from_if_empty() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: TableDefinition = TableDefinition::new("x"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(definition).unwrap(); + for i in 0..1000 { + table.insert(i, i).unwrap(); + } + #[expect(clippy::reversed_empty_ranges)] + let mut iter = table.extract_from_if(500..0, |_, _| true).unwrap(); + assert!(iter.next().is_none()); + } + write_txn.commit().unwrap(); +} + +#[test] +fn retain_in_empty() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: TableDefinition = TableDefinition::new("x"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(definition).unwrap(); + for i in 0..1000 { + table.insert(i, i).unwrap(); + } + #[expect(clippy::reversed_empty_ranges)] + table.retain_in(500..0, |_, _| false).unwrap(); + assert_eq!(table.len().unwrap(), 1000); + } + write_txn.commit().unwrap(); +} + +#[test] +fn range_arc() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: TableDefinition<&str, &str> = TableDefinition::new("x"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(definition).unwrap(); + table.insert("hello", "world").unwrap(); + } + write_txn.commit().unwrap(); + + let mut iter = { + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(definition).unwrap(); + let start = "hello".to_string(); + table.range::<&str>(start.as_str()..).unwrap() + }; + assert_eq!(iter.next().unwrap().unwrap().1.value(), "world"); + assert!(iter.next().is_none()); +} + +#[test] +fn range_clone() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: TableDefinition<&str, &str> = TableDefinition::new("x"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(definition).unwrap(); + table.insert("hello", "world").unwrap(); + let mut iter1 = table.iter().unwrap(); + let mut iter2 = iter1.clone(); + let (k1, v1) = iter1.next().unwrap().unwrap(); + let (k2, v2) = iter2.next().unwrap().unwrap(); + assert_eq!(k1.value(), k2.value()); + assert_eq!(v1.value(), v2.value()); + } + write_txn.commit().unwrap(); +} + +#[test] +fn custom_ordering() { + #[derive(Debug)] + struct ReverseKey(Vec); + + impl Value for ReverseKey { + type SelfType<'a> + = ReverseKey + where + Self: 'a; + type AsBytes<'a> + = &'a [u8] + where + Self: 'a; + + fn fixed_width() -> Option { + None + } + + fn from_bytes<'a>(data: &'a [u8]) -> ReverseKey + where + Self: 'a, + { + ReverseKey(data.to_vec()) + } + + fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> &'a [u8] + where + Self: 'a, + Self: 'b, + { + &value.0 + } + + fn type_name() -> TypeName { + TypeName::new("test::ReverseKey") + } + } + + impl Key for ReverseKey { + fn compare(data1: &[u8], data2: &[u8]) -> Ordering { + data2.cmp(data1) + } + } + + let definition: TableDefinition = TableDefinition::new("x"); + + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(definition).unwrap(); + for i in 0..10u8 { + let key = vec![i]; + table.insert(&ReverseKey(key), "value").unwrap(); + } + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(definition).unwrap(); + let start = ReverseKey(vec![7u8]); // ReverseKey is used, so 7 < 3 + let end = ReverseKey(vec![3u8]); + let mut iter = table.range(start..=end).unwrap(); + for i in (3..=7u8).rev() { + let (key, value) = iter.next().unwrap().unwrap(); + assert_eq!(&[i], key.value().0.as_slice()); + assert_eq!("value", value.value()); + } + assert!(iter.next().is_none()); +} + +#[test] +fn owned_get_signatures() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: TableDefinition = TableDefinition::new("x"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(definition).unwrap(); + for i in 0..10 { + table.insert(&i, &(i + 1)).unwrap(); + } + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(definition).unwrap(); + + assert_eq!(2, table.get(&1).unwrap().unwrap().value()); + + let mut iter: Range = table.range::(..).unwrap(); + for i in 0..10 { + assert_eq!(iter.next().unwrap().unwrap().1.value(), i + 1); + } + assert!(iter.next().is_none()); + let mut iter: Range = table.range(0..10).unwrap(); + for i in 0..10 { + assert_eq!(iter.next().unwrap().unwrap().1.value(), i + 1); + } + assert!(iter.next().is_none()); + let mut iter = table.range::<&u32>(&0..&10).unwrap(); + for i in 0..10 { + assert_eq!(iter.next().unwrap().unwrap().1.value(), i + 1); + } + assert!(iter.next().is_none()); +} + +#[test] +fn ref_get_signatures() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(SLICE_TABLE).unwrap(); + for i in 0..10u8 { + table.insert([i].as_slice(), [i + 1].as_slice()).unwrap(); + } + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(SLICE_TABLE).unwrap(); + + let zero = vec![0u8]; + assert_eq!(&[1], table.get([0].as_slice()).unwrap().unwrap().value()); + assert_eq!(&[1], table.get(b"\0".as_slice()).unwrap().unwrap().value()); + assert_eq!(&[1], table.get(zero.as_slice()).unwrap().unwrap().value()); + + let start = vec![0u8]; + let end = vec![10u8]; + let mut iter = table.range::<&[u8]>(..).unwrap(); + for i in 0..10 { + assert_eq!(iter.next().unwrap().unwrap().1.value(), &[i + 1]); + } + assert!(iter.next().is_none()); + + let mut iter = table.range(start.as_slice()..&end).unwrap(); + for i in 0..10 { + assert_eq!(iter.next().unwrap().unwrap().1.value(), &[i + 1]); + } + assert!(iter.next().is_none()); + drop(iter); + + let mut iter = table.range(start.as_slice()..end.as_slice()).unwrap(); + for i in 0..10 { + assert_eq!(iter.next().unwrap().unwrap().1.value(), &[i + 1]); + } + assert!(iter.next().is_none()); + + let mut iter = table.range([0u8].as_slice()..[10u8].as_slice()).unwrap(); + for i in 0..10u8 { + assert_eq!(iter.next().unwrap().unwrap().1.value(), [i + 1].as_slice()); + } + assert!(iter.next().is_none()); +} + +#[cfg(not(target_os = "wasi"))] +#[test] +fn concurrent_write_transactions_block() { + let tmpfile = create_tempfile(); + let db = sync::Arc::new(Database::create(tmpfile.path()).unwrap()); + let wtx = db.begin_write().unwrap(); + let (sender, receiver) = sync::mpsc::channel(); + + let t = { + std::thread::spawn(move || { + sender.send(()).unwrap(); + db.begin_write().unwrap().commit().unwrap(); + }) + }; + + receiver.recv().unwrap(); + std::thread::sleep(std::time::Duration::from_secs(1)); + wtx.commit().unwrap(); + t.join().unwrap(); +} + +#[test] +fn iter() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(U64_TABLE).unwrap(); + for i in 0..10 { + table.insert(&i, &i).unwrap(); + } + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(U64_TABLE).unwrap(); + let mut iter = table.iter().unwrap(); + for i in 0..10 { + let (k, v) = iter.next().unwrap().unwrap(); + assert_eq!(i, k.value()); + assert_eq!(i, v.value()); + } +} + +#[test] +fn signature_lifetimes() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + + let _ = { + let key = "hi".to_string(); + let value = "1".to_string(); + table.insert(key.as_str(), value.as_str()).unwrap() + }; + + let _ = { + let key = "hi".to_string(); + table.get(key.as_str()).unwrap() + }; + + let _ = { + let key = "hi".to_string(); + table.remove(key.as_str()).unwrap() + }; + + let _ = { + let key = "hi".to_string(); + table.range(key.as_str()..).unwrap() + }; + + let _ = { table.extract_if(|_, _| true).unwrap() }; + + let _ = { + let key = "hi".to_string(); + table.extract_from_if(key.as_str().., |_, _| true).unwrap() + }; + } + write_txn.commit().unwrap(); +} + +#[test] +fn generic_signature_lifetimes() { + fn write_key_generic( + table: TableDefinition, + key: K::SelfType<'_>, + db: &Database, + ) { + let buf = [1, 2, 3]; + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(table).unwrap(); + table.insert(key, buf.as_slice()).unwrap(); + } + write_txn.commit().unwrap(); + } + + fn read_key_generic( + table: TableDefinition, + key: K::SelfType<'_>, + db: &Database, + ) { + let buf = [1, 2, 3]; + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(table).unwrap(); + assert_eq!(table.get(key).unwrap().unwrap().value(), buf); + } + + let tmpfile = create_tempfile(); + let db = &Database::create(tmpfile.path()).unwrap(); + { + let (table, key) = (TableDefinition::<&str, _>::new("&str"), "key"); + write_key_generic(table, key, db); + read_key_generic(table, key, db); + } + { + let (table, key) = (TableDefinition::<(), _>::new("()"), ()); + write_key_generic(table, key, db); + read_key_generic(table, key, db); + } +} + +#[test] +fn char_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: TableDefinition = TableDefinition::new("x"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(definition).unwrap(); + table.insert('a', &'b').unwrap(); + table.insert(&'b', 'a').unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(definition).unwrap(); + assert_eq!('a', table.get(&'b').unwrap().unwrap().value()); + assert_eq!('b', table.get(&'a').unwrap().unwrap().value()); + + let mut iter = table.iter().unwrap(); + assert_eq!(iter.next().unwrap().unwrap().0.value(), 'a'); + assert_eq!(iter.next().unwrap().unwrap().0.value(), 'b'); + assert!(iter.next().is_none()); +} + +// Test that &[u8; N] and [u8; N] are effectively the same +#[test] +fn u8_array_serialization() { + assert_eq!( + <&[u8; 7] as Value>::type_name(), + <[u8; 7] as Value>::type_name() + ); + let fixed_value: u128 = random(); + let fixed_serialized = fixed_value.to_le_bytes(); + for _ in 0..1000 { + let x: u128 = random(); + let x_serialized = x.to_le_bytes(); + let ref_x_serialized = &x_serialized; + let u8_ref_serialized = <&[u8; 16] as Value>::as_bytes(&ref_x_serialized); + let u8_generic_serialized = <[u8; 16] as Value>::as_bytes(&x_serialized); + assert_eq!( + u8_ref_serialized.as_slice(), + u8_generic_serialized.as_slice() + ); + assert_eq!(u8_ref_serialized.as_slice(), x_serialized.as_slice()); + let ref_order = <&[u8; 16] as Key>::compare(&x_serialized, &fixed_serialized); + let generic_order = <[u8; 16] as Key>::compare(&x_serialized, &fixed_serialized); + assert_eq!(ref_order, generic_order); + } +} diff --git a/v2/redb/tests/integration_tests.rs b/v2/redb/tests/integration_tests.rs new file mode 100644 index 000000000..bf6f43825 --- /dev/null +++ b/v2/redb/tests/integration_tests.rs @@ -0,0 +1,2916 @@ +use rand::RngExt; +use rand::prelude::SliceRandom; +use redb::backends::FileBackend; +use redb::{ + AccessGuard, Builder, CompactionError, Database, Durability, Key, MultimapRange, + MultimapTableDefinition, MultimapValue, Range, ReadableDatabase, ReadableTable, + ReadableTableMetadata, SetDurabilityError, StorageBackend, TableDefinition, TableStats, + TransactionError, Value, +}; +use redb::{DatabaseError, ReadableMultimapTable, SavepointError, StorageError, TableError}; +use std::borrow::Borrow; +use std::fs; +use std::io::{ErrorKind, Write}; +use std::marker::PhantomData; +use std::ops::RangeBounds; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; + +const ELEMENTS: usize = 100; + +const SLICE_TABLE: TableDefinition<&[u8], &[u8]> = TableDefinition::new("slice"); +const SLICE_TABLE2: TableDefinition<&[u8], &[u8]> = TableDefinition::new("slice2"); +const STR_TABLE: TableDefinition<&str, &str> = TableDefinition::new("x"); +const U64_TABLE: TableDefinition = TableDefinition::new("u64"); + +fn create_tempfile() -> tempfile::NamedTempFile { + if cfg!(target_os = "wasi") { + tempfile::NamedTempFile::new_in("/tmp").unwrap() + } else { + tempfile::NamedTempFile::new().unwrap() + } +} + +/// Returns pairs of key, value +fn random_data(count: usize, key_size: usize, value_size: usize) -> Vec<(Vec, Vec)> { + let mut pairs = vec![]; + + for _ in 0..count { + let key: Vec = (0..key_size).map(|_| rand::rng().random()).collect(); + let value: Vec = (0..value_size).map(|_| rand::rng().random()).collect(); + pairs.push((key, value)); + } + + pairs +} + +#[test] +fn previous_io_error() { + #[derive(Debug)] + struct FailingBackend { + inner: FileBackend, + fail_flag: Arc, + } + + impl FailingBackend { + fn new(backend: FileBackend, fail_flag: Arc) -> Self { + Self { + inner: backend, + fail_flag, + } + } + } + + impl StorageBackend for FailingBackend { + fn len(&self) -> Result { + self.inner.len() + } + + fn read(&self, offset: u64, out: &mut [u8]) -> Result<(), std::io::Error> { + self.inner.read(offset, out) + } + + fn set_len(&self, len: u64) -> Result<(), std::io::Error> { + self.inner.set_len(len) + } + + fn sync_data(&self) -> Result<(), std::io::Error> { + if self.fail_flag.load(Ordering::SeqCst) { + Err(std::io::Error::from(ErrorKind::Other)) + } else { + self.inner.sync_data() + } + } + + fn write(&self, offset: u64, data: &[u8]) -> Result<(), std::io::Error> { + self.inner.write(offset, data) + } + } + + let tmpfile = create_tempfile(); + + let fail_flag = Arc::new(AtomicBool::new(false)); + let backend = FailingBackend::new( + FileBackend::new(tmpfile.into_file()).unwrap(), + fail_flag.clone(), + ); + let db = Database::builder().create_with_backend(backend).unwrap(); + fail_flag.store(true, Ordering::SeqCst); + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.insert(&0, &0).unwrap(); + } + assert!(txn.commit().is_err()); + + assert!(matches!( + db.begin_write().err().unwrap(), + TransactionError::Storage(StorageError::PreviousIo) + )); +} + +#[test] +fn mixed_durable_commit() { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + let mut txn = db.begin_write().unwrap(); + txn.set_durability(Durability::None).unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.insert(&0, &0).unwrap(); + } + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + txn.commit().unwrap(); +} + +#[test] +fn non_durable_commit_persistence() { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + let mut txn = db.begin_write().unwrap(); + txn.set_durability(Durability::None).unwrap(); + let pairs = random_data(100, 16, 20); + { + let mut table = txn.open_table(SLICE_TABLE).unwrap(); + for i in 0..ELEMENTS { + let (key, value) = &pairs[i % pairs.len()]; + table.insert(key.as_slice(), value.as_slice()).unwrap(); + } + } + txn.commit().unwrap(); + + // Check that cleanly closing the database persists the non-durable commit + drop(db); + let db = Database::create(tmpfile.path()).unwrap(); + let txn = db.begin_read().unwrap(); + let table = txn.open_table(SLICE_TABLE).unwrap(); + + let mut key_order: Vec = (0..ELEMENTS).collect(); + key_order.shuffle(&mut rand::rng()); + + { + for i in &key_order { + let (key, value) = &pairs[*i % pairs.len()]; + assert_eq!(table.get(key.as_slice()).unwrap().unwrap().value(), value); + } + } +} + +fn test_persistence(durability: Durability) { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + let mut txn = db.begin_write().unwrap(); + txn.set_durability(durability).unwrap(); + let pairs = random_data(100, 16, 20); + { + let mut table = txn.open_table(SLICE_TABLE).unwrap(); + for i in 0..ELEMENTS { + let (key, value) = &pairs[i % pairs.len()]; + table.insert(key.as_slice(), value.as_slice()).unwrap(); + } + } + txn.commit().unwrap(); + + drop(db); + let db = Database::create(tmpfile.path()).unwrap(); + let txn = db.begin_read().unwrap(); + let table = txn.open_table(SLICE_TABLE).unwrap(); + + let mut key_order: Vec = (0..ELEMENTS).collect(); + key_order.shuffle(&mut rand::rng()); + + { + for i in &key_order { + let (key, value) = &pairs[*i % pairs.len()]; + assert_eq!(table.get(key.as_slice()).unwrap().unwrap().value(), value); + } + } +} + +#[test] +fn immediate_persistence() { + test_persistence(Durability::Immediate); +} + +#[test] +fn immediate_free() { + test_free(Durability::Immediate); +} + +#[test] +fn nondurable_free() { + test_free(Durability::None); +} + +fn test_free(durability: Durability) { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + let mut txn = db.begin_write().unwrap(); + txn.set_durability(durability).unwrap(); + { + let _table = txn.open_table(SLICE_TABLE).unwrap(); + let mut table = txn.open_table(SLICE_TABLE2).unwrap(); + table.insert([].as_slice(), [].as_slice()).unwrap(); + } + txn.commit().unwrap(); + let mut txn = db.begin_write().unwrap(); + txn.set_durability(durability).unwrap(); + { + let mut table = txn.open_table(SLICE_TABLE2).unwrap(); + table.remove([].as_slice()).unwrap(); + } + txn.commit().unwrap(); + let mut txn = db.begin_write().unwrap(); + txn.set_durability(durability).unwrap(); + txn.commit().unwrap(); + + let mut txn = db.begin_write().unwrap(); + txn.set_durability(durability).unwrap(); + let allocated_pages = txn.stats().unwrap().allocated_pages(); + + let key = vec![0; 100]; + let value = vec![0u8; 1024]; + let target_db_size = 8 * 1024 * 1024; + // Write 10% of db space each iteration + let num_writes = target_db_size / 10 / (key.len() + value.len()); + // Make sure an internal index page is required + assert!(num_writes > 64); + + { + let mut table = txn.open_table(SLICE_TABLE).unwrap(); + for i in 0..num_writes { + let mut mut_key = key.clone(); + mut_key.extend_from_slice(&(i as u64).to_le_bytes()); + table.insert(mut_key.as_slice(), value.as_slice()).unwrap(); + } + } + txn.commit().unwrap(); + + { + let key_range: Vec = (0..num_writes).collect(); + // Delete in chunks to be sure that we don't run out of pages due to temp allocations + for chunk in key_range.chunks(10) { + let mut txn = db.begin_write().unwrap(); + txn.set_durability(durability).unwrap(); + { + let mut table = txn.open_table(SLICE_TABLE).unwrap(); + for i in chunk { + let mut mut_key = key.clone(); + mut_key.extend_from_slice(&(*i as u64).to_le_bytes()); + table.remove(mut_key.as_slice()).unwrap(); + } + } + txn.commit().unwrap(); + } + } + + // Extra commit to finalize the cleanup of the freed pages + let mut txn = db.begin_write().unwrap(); + txn.set_durability(durability).unwrap(); + txn.commit().unwrap(); + let mut txn = db.begin_write().unwrap(); + txn.set_durability(durability).unwrap(); + assert_eq!(allocated_pages, txn.stats().unwrap().allocated_pages()); + txn.abort().unwrap(); +} + +#[test] +fn nondurable_live_and_free() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.insert(0, 0).unwrap(); + } + txn.commit().unwrap(); + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.remove(0).unwrap(); + } + txn.commit().unwrap(); + // Process frees + let txn = db.begin_write().unwrap(); + txn.commit().unwrap(); + let txn = db.begin_write().unwrap(); + txn.commit().unwrap(); + let txn = db.begin_write().unwrap(); + let allocated_pages = txn.stats().unwrap().allocated_pages(); + txn.abort().unwrap(); + + let mut txn = db.begin_write().unwrap(); + txn.set_durability(Durability::None).unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.insert(0, 1).unwrap(); + } + txn.commit().unwrap(); + let read_txn = db.begin_read().unwrap(); + + for i in 0..5 { + let mut txn = db.begin_write().unwrap(); + txn.set_durability(Durability::None).unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.insert(0, i).unwrap(); + } + txn.commit().unwrap(); + } + + { + let table = read_txn.open_table(U64_TABLE).unwrap(); + assert_eq!(table.get(0).unwrap().unwrap().value(), 1); + } + drop(read_txn); + + let mut txn = db.begin_write().unwrap(); + txn.set_durability(Durability::None).unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.remove(0).unwrap(); + } + txn.commit().unwrap(); + + let mut txn = db.begin_write().unwrap(); + txn.set_durability(Durability::None).unwrap(); + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + // allocated * 2, because we can't free the original persisted pages + // + 2, because now we need freed trees to store those original pages to be freed + assert!(txn.stats().unwrap().allocated_pages() <= allocated_pages * 2 + 2); +} + +#[test] +fn large_values() { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + let txn = db.begin_write().unwrap(); + + let mut key = vec![0u8; 1024]; + let value = vec![0u8; 2_000_000]; + { + let mut table = txn.open_table(SLICE_TABLE).unwrap(); + for i in 0..5 { + key[0] = i; + table.insert(key.as_slice(), value.as_slice()).unwrap(); + } + } + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(SLICE_TABLE).unwrap(); + for i in 0..5 { + key[0] = i; + table.remove(key.as_slice()).unwrap(); + } + } + txn.commit().unwrap(); +} + +// Note: this test requires > 3GiB of memory +#[test] +#[cfg(target_pointer_width = "64")] +fn value_too_large() { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + let txn = db.begin_write().unwrap(); + + let small_value = vec![0u8; 1024]; + let too_big_value = vec![0u8; 3 * 1024 * 1024 * 1024 + 1]; + { + let mut table = txn.open_table(SLICE_TABLE).unwrap(); + assert!(matches!( + table.insert(small_value.as_slice(), too_big_value.as_slice()), + Err(StorageError::ValueTooLarge(_)) + )); + assert!(matches!( + table.insert(too_big_value.as_slice(), small_value.as_slice()), + Err(StorageError::ValueTooLarge(_)) + )); + assert!(matches!( + table.insert(too_big_value.as_slice(), too_big_value.as_slice()), + Err(StorageError::ValueTooLarge(_)) + )); + drop(too_big_value); + let almost_big_value = vec![0u8; 2 * 1024 * 1024 * 1024]; + assert!(matches!( + table.insert(almost_big_value.as_slice(), almost_big_value.as_slice()), + Err(StorageError::ValueTooLarge(_)) + )); + } + txn.commit().unwrap(); + + let txn = db.begin_read().unwrap(); + let table = txn.open_table(SLICE_TABLE).unwrap(); + assert!(table.is_empty().unwrap()); +} + +#[test] +fn small_db_is_small_file() { + let tmpfile = create_tempfile(); + const TABLE: TableDefinition = TableDefinition::new("TABLE"); + + let mut db = Database::create(tmpfile.path()).unwrap(); + let wtx = db.begin_write().unwrap(); + let mut table = wtx.open_table(TABLE).unwrap(); + table.insert(0, 0).unwrap(); + drop(table); + wtx.commit().unwrap(); + + db.compact().unwrap(); + + drop(db); + let metadata = tmpfile.as_file().metadata().unwrap(); + assert!( + metadata.len() < 40 * 1024, + "File size: {:?}", + metadata.len() + ); +} + +#[test] +fn many_pairs() { + let tmpfile = create_tempfile(); + const TABLE: TableDefinition = TableDefinition::new("TABLE"); + + let db = Database::create(tmpfile.path()).unwrap(); + let wtx = db.begin_write().unwrap(); + + let mut table = wtx.open_table(TABLE).unwrap(); + + for i in 0..200_000 { + table.insert(i, i).unwrap(); + + if i % 10_000 == 0 { + eprintln!("{i}"); + } + } + + drop(table); + + wtx.commit().unwrap(); +} + +#[test] +fn explicit_close() { + let tmpfile = create_tempfile(); + const TABLE: TableDefinition = TableDefinition::new("TABLE"); + let db = Database::create(tmpfile.path()).unwrap(); + let wtx = db.begin_write().unwrap(); + wtx.open_table(TABLE).unwrap(); + wtx.commit().unwrap(); + + let tx = db.begin_read().unwrap(); + let table = tx.open_table(TABLE).unwrap(); + assert!(matches!( + tx.close(), + Err(TransactionError::ReadTransactionStillInUse(_)) + )); + drop(table); + + let tx2 = db.begin_read().unwrap(); + tx2.close().unwrap(); +} + +#[test] +fn large_keys() { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + let txn = db.begin_write().unwrap(); + + let mut key = vec![0u8; 1024]; + let value = vec![0u8; 1]; + { + let mut table = txn.open_table(SLICE_TABLE).unwrap(); + for i in 0..100 { + key[0] = i; + table.insert(key.as_slice(), value.as_slice()).unwrap(); + } + } + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(SLICE_TABLE).unwrap(); + for i in 0..100 { + key[0] = i; + table.remove(key.as_slice()).unwrap(); + } + } + txn.commit().unwrap(); +} + +#[test] +fn dynamic_growth() { + let tmpfile = create_tempfile(); + let table_definition: TableDefinition = TableDefinition::new("x"); + let big_value = vec![0u8; 1024]; + + let expected_size = 10 * 1024 * 1024; + let db = Database::create(tmpfile.path()).unwrap(); + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(table_definition).unwrap(); + table.insert(&0, big_value.as_slice()).unwrap(); + } + txn.commit().unwrap(); + + let initial_file_size = tmpfile.as_file().metadata().unwrap().len(); + assert!(initial_file_size < (expected_size / 2) as u64); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(table_definition).unwrap(); + for i in 0..2048 { + table.insert(&i, big_value.as_slice()).unwrap(); + } + } + txn.commit().unwrap(); + + let file_size = tmpfile.as_file().metadata().unwrap().len(); + + assert!(file_size > initial_file_size); +} + +#[test] +fn multi_page_kv() { + let tmpfile = create_tempfile(); + let elements = 4; + let page_size = 4096; + + let db = Builder::new().create(tmpfile.path()).unwrap(); + let txn = db.begin_write().unwrap(); + + let mut key = vec![0u8; page_size + 1]; + let mut value = vec![0; page_size + 1]; + { + let mut table = txn.open_table(SLICE_TABLE).unwrap(); + for i in 0..elements { + key[0] = i; + value[0] = i; + table.insert(key.as_slice(), value.as_slice()).unwrap(); + } + } + txn.commit().unwrap(); + + let txn = db.begin_read().unwrap(); + let table = txn.open_table(SLICE_TABLE).unwrap(); + for i in 0..elements { + key[0] = i; + value[0] = i; + assert_eq!(&value, table.get(key.as_slice()).unwrap().unwrap().value()); + } + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(SLICE_TABLE).unwrap(); + for i in 0..elements { + key[0] = i; + table.remove(key.as_slice()).unwrap(); + } + } + txn.commit().unwrap(); +} + +#[test] +// Test for a bug in the deletion code, where deleting a key accidentally deleted other keys +fn regression() { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.insert(&1, &1).unwrap(); + } + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.insert(&6, &9).unwrap(); + } + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.insert(&12, &10).unwrap(); + } + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.insert(&18, &27).unwrap(); + } + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.insert(&24, &33).unwrap(); + } + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.insert(&30, &14).unwrap(); + } + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.remove(&30).unwrap(); + } + txn.commit().unwrap(); + + let txn = db.begin_read().unwrap(); + let table = txn.open_table(U64_TABLE).unwrap(); + let v = table.get(&6).unwrap().unwrap().value(); + assert_eq!(v, 9); +} + +#[test] +// Test for a bug in table creation code, where multiple tables could end up with the same id +fn regression2() { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + let tx = db.begin_write().unwrap(); + + let a_def: TableDefinition<&str, &str> = TableDefinition::new("a"); + let b_def: TableDefinition<&str, &str> = TableDefinition::new("b"); + let c_def: TableDefinition<&str, &str> = TableDefinition::new("c"); + + let _c = tx.open_table(c_def).unwrap(); + let b = tx.open_table(b_def).unwrap(); + let mut a = tx.open_table(a_def).unwrap(); + a.insert("hi", "1").unwrap(); + assert!(b.get("hi").unwrap().is_none()); +} + +#[test] +// Test for a bug in deletion code, where deletions could delete neighboring keys in a leaf, +// due to the partial leaf entries being dropped +fn regression3() { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + let tx = db.begin_write().unwrap(); + { + let mut t = tx.open_table(SLICE_TABLE).unwrap(); + let big_value = vec![0u8; 1000]; + for i in 0..20u8 { + t.insert([i].as_slice(), big_value.as_slice()).unwrap(); + } + for i in (10..20u8).rev() { + t.remove([i].as_slice()).unwrap(); + for j in 0..i { + assert!(t.get([j].as_slice()).unwrap().is_some()); + } + } + } + tx.commit().unwrap(); +} + +#[test] +fn regression7() { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition = TableDefinition::new("x"); + + let tx = db.begin_write().unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + let big_value = vec![0u8; 4063]; + t.insert(&35723, big_value.as_slice()).unwrap(); + t.remove(&145278).unwrap(); + t.remove(&145227).unwrap(); + } + tx.commit().unwrap(); + + let mut tx = db.begin_write().unwrap(); + tx.set_durability(Durability::None).unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + let v = vec![0u8; 47]; + t.insert(&66469, v.as_slice()).unwrap(); + let v = vec![0u8; 2414]; + t.insert(&146255, v.as_slice()).unwrap(); + let v = vec![0u8; 159]; + t.insert(&153701, v.as_slice()).unwrap(); + let v = vec![0u8; 1186]; + t.insert(&145227, v.as_slice()).unwrap(); + let v = vec![0u8; 223]; + t.insert(&118749, v.as_slice()).unwrap(); + + t.remove(&145227).unwrap(); + + let mut iter = t.range(138763..(138763 + 232359)).unwrap().rev(); + assert_eq!(iter.next().unwrap().unwrap().0.value(), 153701); + assert_eq!(iter.next().unwrap().unwrap().0.value(), 146255); + assert!(iter.next().is_none()); + } + tx.commit().unwrap(); +} + +#[test] +fn regression8() { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition = TableDefinition::new("x"); + + let mut tx = db.begin_write().unwrap(); + tx.set_durability(Durability::None).unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + let v = vec![0u8; 1186]; + t.insert(&145227, v.as_slice()).unwrap(); + let v = vec![0u8; 1585]; + t.insert(&565922, v.as_slice()).unwrap(); + } + tx.commit().unwrap(); + + let tx = db.begin_write().unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + let v = vec![0u8; 2040]; + t.insert(&94937, v.as_slice()).unwrap(); + let v = vec![0u8; 2058]; + t.insert(&130571, v.as_slice()).unwrap(); + t.remove(&145227).unwrap(); + } + tx.commit().unwrap(); + + let tx = db.begin_write().unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + let v = vec![0u8; 947]; + t.insert(&118749, v.as_slice()).unwrap(); + } + tx.commit().unwrap(); + + let tx = db.begin_write().unwrap(); + { + let t = tx.open_table(table_def).unwrap(); + let mut iter = t.range(118749..142650).unwrap(); + assert_eq!(iter.next().unwrap().unwrap().0.value(), 118749); + assert_eq!(iter.next().unwrap().unwrap().0.value(), 130571); + assert!(iter.next().is_none()); + } + tx.commit().unwrap(); +} + +#[test] +fn regression9() { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition = TableDefinition::new("x"); + + let tx = db.begin_write().unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + let v = vec![0u8; 118665]; + t.insert(&452, v.as_slice()).unwrap(); + t.len().unwrap(); + } + tx.commit().unwrap(); +} + +#[test] +fn regression10() { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition = TableDefinition::new("x"); + + let tx = db.begin_write().unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + let v = vec![0u8; 1043]; + t.insert(&118749, v.as_slice()).unwrap(); + } + tx.commit().unwrap(); + + let tx = db.begin_write().unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + let v = vec![0u8; 952]; + t.insert(&118757, v.as_slice()).unwrap(); + } + tx.abort().unwrap(); + + let tx = db.begin_write().unwrap(); + { + let t = tx.open_table(table_def).unwrap(); + t.get(&829513).unwrap(); + } + tx.abort().unwrap(); +} + +#[test] +fn regression11() { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition = TableDefinition::new("x"); + + let tx = db.begin_write().unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + let v = vec![0u8; 1204]; + t.insert(&118749, v.as_slice()).unwrap(); + let v = vec![0u8; 2062]; + t.insert(&153697, v.as_slice()).unwrap(); + let v = vec![0u8; 2980]; + t.insert(&110557, v.as_slice()).unwrap(); + let v = vec![0u8; 1999]; + t.insert(&677853, v.as_slice()).unwrap(); + } + tx.commit().unwrap(); + + let tx = db.begin_write().unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + let v = vec![0u8; 691]; + t.insert(&103591, v.as_slice()).unwrap(); + let v = vec![0u8; 952]; + t.insert(&118757, v.as_slice()).unwrap(); + } + tx.abort().unwrap(); + + let tx = db.begin_write().unwrap(); + tx.commit().unwrap(); +} + +#[test] +// Test that for stale read bug when re-opening a table during a write +fn regression12() { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition = TableDefinition::new("x"); + + let tx = db.begin_write().unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + t.insert(&0, &0).unwrap(); + assert_eq!(t.get(&0).unwrap().unwrap().value(), 0); + drop(t); + + let t2 = tx.open_table(table_def).unwrap(); + assert_eq!(t2.get(&0).unwrap().unwrap().value(), 0); + } + tx.commit().unwrap(); +} + +#[test] +fn regression13() { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def: MultimapTableDefinition = MultimapTableDefinition::new("x"); + + let mut tx = db.begin_write().unwrap(); + tx.set_durability(Durability::None).unwrap(); + { + let mut t = tx.open_multimap_table(table_def).unwrap(); + let value = vec![0; 1026]; + t.insert(&539717, value.as_slice()).unwrap(); + let value = vec![0; 530]; + t.insert(&539717, value.as_slice()).unwrap(); + } + tx.abort().unwrap(); +} + +#[test] +fn regression14() { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + + let table_def: MultimapTableDefinition = MultimapTableDefinition::new("x"); + + let mut tx = db.begin_write().unwrap(); + tx.set_durability(Durability::None).unwrap(); + { + let mut t = tx.open_multimap_table(table_def).unwrap(); + let value = vec![0; 1424]; + t.insert(&539749, value.as_slice()).unwrap(); + } + tx.commit().unwrap(); + + let mut tx = db.begin_write().unwrap(); + tx.set_durability(Durability::None).unwrap(); + { + let mut t = tx.open_multimap_table(table_def).unwrap(); + let value = vec![0; 2230]; + t.insert(&776971, value.as_slice()).unwrap(); + + let mut iter = t.range(514043..(514043 + 514043)).unwrap().rev(); + { + let (key, mut value_iter) = iter.next().unwrap().unwrap(); + assert_eq!(key.value(), 776971); + assert_eq!(value_iter.next().unwrap().unwrap().value(), &[0; 2230]); + } + { + let (key, mut value_iter) = iter.next().unwrap().unwrap(); + assert_eq!(key.value(), 539749); + assert_eq!(value_iter.next().unwrap().unwrap().value(), &[0; 1424]); + } + } + tx.abort().unwrap(); +} + +#[test] +fn regression17() { + let tmpfile = create_tempfile(); + + let db = Database::builder().create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition = TableDefinition::new("x"); + + let mut tx = db.begin_write().unwrap(); + tx.set_durability(Durability::None).unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + let value = vec![0; 4578]; + t.insert(&671325, value.as_slice()).unwrap(); + + let mut value = t.insert_reserve(&723904, 2246).unwrap(); + value.as_mut().fill(0xFF); + } + tx.abort().unwrap(); +} + +#[test] +fn regression18() { + let tmpfile = create_tempfile(); + + let db = Database::builder().create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition = TableDefinition::new("x"); + + let tx = db.begin_write().unwrap(); + let savepoint0 = tx.ephemeral_savepoint().unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + let mut value = t.insert_reserve(&118749, 817).unwrap(); + value.as_mut().fill(0xFF); + } + tx.commit().unwrap(); + + let tx = db.begin_write().unwrap(); + let savepoint1 = tx.ephemeral_savepoint().unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + let mut value = t.insert_reserve(&65373, 1807).unwrap(); + value.as_mut().fill(0xFF); + } + tx.commit().unwrap(); + + let mut tx = db.begin_write().unwrap(); + let savepoint2 = tx.ephemeral_savepoint().unwrap(); + + tx.restore_savepoint(&savepoint2).unwrap(); + tx.commit().unwrap(); + + drop(savepoint0); + + let tx = db.begin_write().unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + let mut value = t.insert_reserve(&118749, 2494).unwrap(); + value.as_mut().fill(0xFF); + } + tx.commit().unwrap(); + + let tx = db.begin_write().unwrap(); + let savepoint4 = tx.ephemeral_savepoint().unwrap(); + tx.abort().unwrap(); + drop(savepoint1); + + let tx = db.begin_write().unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + let mut value = t.insert_reserve(&429469, 667).unwrap(); + value.as_mut().fill(0xFF); + drop(value); + let mut value = t.insert_reserve(&266845, 1614).unwrap(); + value.as_mut().fill(0xFF); + } + tx.commit().unwrap(); + + let mut tx = db.begin_write().unwrap(); + tx.restore_savepoint(&savepoint4).unwrap(); + tx.commit().unwrap(); + + drop(savepoint2); + drop(savepoint4); +} + +#[test] +fn regression19() { + let tmpfile = create_tempfile(); + + let db = Database::builder().create(tmpfile.path()).unwrap(); + + let table_def: TableDefinition = TableDefinition::new("x"); + + let tx = db.begin_write().unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + let value = vec![0xFF; 100]; + t.insert(&1, value.as_slice()).unwrap(); + } + tx.commit().unwrap(); + + let tx = db.begin_write().unwrap(); + let savepoint0 = tx.ephemeral_savepoint().unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + let value = vec![0xFF; 101]; + t.insert(&1, value.as_slice()).unwrap(); + } + tx.commit().unwrap(); + + let tx = db.begin_write().unwrap(); + { + let mut t = tx.open_table(table_def).unwrap(); + let value = vec![0xFF; 102]; + t.insert(&1, value.as_slice()).unwrap(); + } + tx.commit().unwrap(); + + let mut tx = db.begin_write().unwrap(); + tx.restore_savepoint(&savepoint0).unwrap(); + tx.commit().unwrap(); + + let tx = db.begin_write().unwrap(); + tx.open_table(table_def).unwrap(); +} + +#[test] +fn regression20() { + let tmpfile = create_tempfile(); + + let table_def: MultimapTableDefinition<'static, u128, u128> = + MultimapTableDefinition::new("some-table"); + + for _ in 0..3 { + let mut db = Database::builder().create(tmpfile.path()).unwrap(); + db.check_integrity().unwrap(); + + let txn = db.begin_write().unwrap(); + let mut table = txn.open_multimap_table(table_def).unwrap(); + + for i in 0..1024 { + table.insert(0, i).unwrap(); + } + drop(table); + + txn.commit().unwrap(); + } +} + +#[test] +fn regression21() { + let tmpfile = create_tempfile(); + + let mut db = Database::create(tmpfile.path()).unwrap(); + + let write_tx = db.begin_write().unwrap(); + let read_tx = db.begin_read().unwrap(); + + let mut write_table = write_tx + .open_table::<&str, &str>(TableDefinition::new("example")) + .unwrap(); + + write_table.insert("example", "example").unwrap(); + + drop(write_table); + + write_tx.commit().unwrap(); + assert!(matches!( + db.compact().unwrap_err(), + CompactionError::TransactionInProgress + )); + drop(read_tx); +} + +#[test] +fn regression22() { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.insert(0, 0).unwrap(); + } + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.remove(0).unwrap(); + } + txn.commit().unwrap(); + + // Extra commit to finalize the cleanup of the freed pages + let txn = db.begin_write().unwrap(); + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + let allocated_pages = txn.stats().unwrap().allocated_pages(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.insert(0, 0).unwrap(); + } + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.remove(0).unwrap(); + } + txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + + // Extra commit to finalize the cleanup of the freed pages. The read transaction should not + // block the freeing, but there was a bug where it did. + db.begin_write().unwrap().commit().unwrap(); + + drop(read_txn); + + let txn = db.begin_write().unwrap(); + assert_eq!(allocated_pages, txn.stats().unwrap().allocated_pages()); +} + +#[test] +fn regression23() { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + let txn = db.begin_write().unwrap(); + { + // List the savepoints to ensure the system table is created and occupies a page + #[allow(unused_must_use)] + { + txn.list_persistent_savepoints().unwrap(); + } + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.insert(0, 0).unwrap(); + } + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.remove(0).unwrap(); + } + txn.commit().unwrap(); + + // Extra commit to finalize the cleanup of the freed pages + let txn = db.begin_write().unwrap(); + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + let allocated_pages = txn.stats().unwrap().allocated_pages(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.insert(0, 0).unwrap(); + } + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.remove(0).unwrap(); + } + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + let savepoint = txn.ephemeral_savepoint().unwrap(); + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.insert(0, 0).unwrap(); + } + txn.commit().unwrap(); + + let mut txn = db.begin_write().unwrap(); + txn.restore_savepoint(&savepoint).unwrap(); + txn.commit().unwrap(); + drop(savepoint); + + // Extra commit to finalize the cleanup of the freed pages. + // There was a bug where the restoration of the savepoint would leak pages + db.begin_write().unwrap().commit().unwrap(); + db.begin_write().unwrap().commit().unwrap(); + + let txn = db.begin_write().unwrap(); + assert_eq!(allocated_pages, txn.stats().unwrap().allocated_pages()); +} + +#[test] +fn regression24() { + let tmpfile = create_tempfile(); + + let table_def: MultimapTableDefinition = MultimapTableDefinition::new("x"); + + let db = Database::create(tmpfile.path()).unwrap(); + let txn = db.begin_write().unwrap(); + { + // Touch the savepoints tables to be sure they get created, so that they occupy pages + let id = txn.persistent_savepoint().unwrap(); + txn.delete_persistent_savepoint(id).unwrap(); + // List the savepoints to ensure the system table is created and occupies a page + #[allow(unused_must_use)] + { + txn.list_persistent_savepoints().unwrap(); + } + let mut table = txn.open_table(U64_TABLE).unwrap(); + table.insert(0, 0).unwrap(); + } + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + { + txn.delete_table(U64_TABLE).unwrap(); + } + txn.commit().unwrap(); + + // Extra commit to finalize the cleanup of the freed pages + let txn = db.begin_write().unwrap(); + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + let allocated_pages = txn.stats().unwrap().allocated_pages(); + { + let mut table = txn.open_multimap_table(table_def).unwrap(); + table.insert(0, 0).unwrap(); + } + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + { + txn.delete_multimap_table(table_def).unwrap(); + } + txn.commit().unwrap(); + + // Extra commit to finalize the cleanup of the freed pages. + // There was a bug where deleting a multimap table leaked pages + db.begin_write().unwrap().commit().unwrap(); + + let txn = db.begin_write().unwrap(); + assert_eq!(allocated_pages, txn.stats().unwrap().allocated_pages()); +} + +#[test] +fn regression25() { + let tmpfile = create_tempfile(); + let table_def: TableDefinition = TableDefinition::new("issue_1108"); + + let db = Database::create(tmpfile.path()).unwrap(); + for i in 0..2730u16 { + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(table_def).unwrap(); + for j in 0..24u16 { + let key: u16 = i * 24 + j; + let value = key as u64; + table.insert(key, (value, value, value, value)).unwrap(); + } + } + txn.commit().unwrap(); + } +} + +#[test] +fn regression26() { + let tmpfile = create_tempfile(); + let table_def: TableDefinition = TableDefinition::new("issue_1117"); + + let db = Database::create(tmpfile.path()).unwrap(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(table_def).unwrap(); + table.insert(0, ("name", &[0u8][..])).unwrap(); + } + txn.commit().unwrap(); + + { + let txn = db.begin_write().unwrap(); + let mut table = txn.open_table(table_def).unwrap(); + let mut access = table.get_mut(&0).unwrap().unwrap(); + let name = access.value().0.to_string(); + let large_value = vec![1u8; 8192]; + access.insert((&name[..], large_value.as_slice())).unwrap(); + drop(access); + drop(table); + txn.commit().unwrap(); + } +} + +#[test] +fn regression27() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let def: TableDefinition<&str, &[u8]> = TableDefinition::new("x"); + let value = "world"; + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(def).unwrap(); + let mut reserved = table.insert_reserve("hello", value.len()).unwrap(); + reserved.as_mut().copy_from_slice(value.as_bytes()); + drop(reserved); + drop(table); + write_txn.commit().unwrap(); + } + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(def).unwrap(); + assert_eq!( + value.as_bytes(), + table.get("hello").unwrap().unwrap().value() + ); +} + +#[test] +fn check_integrity_clean() { + let tmpfile = create_tempfile(); + + let table_def: TableDefinition<'static, u64, u64> = TableDefinition::new("x"); + + let mut db = Database::builder().create(tmpfile.path()).unwrap(); + assert!(db.check_integrity().unwrap()); + + let txn = db.begin_write().unwrap(); + let mut table = txn.open_table(table_def).unwrap(); + + for i in 0..10 { + table.insert(0, i).unwrap(); + } + drop(table); + + txn.commit().unwrap(); + assert!(db.check_integrity().unwrap()); + drop(db); + + let mut db = Database::builder().create(tmpfile.path()).unwrap(); + assert!(db.check_integrity().unwrap()); + drop(db); + + let mut db = Database::builder().open(tmpfile.path()).unwrap(); + assert!(db.check_integrity().unwrap()); +} + +#[test] +fn multimap_stats() { + let tmpfile = create_tempfile(); + let db = Database::builder().create(tmpfile.path()).unwrap(); + + let table_def: MultimapTableDefinition = MultimapTableDefinition::new("x"); + + let mut last_size = 0; + for i in 0..1000 { + let mut txn = db.begin_write().unwrap(); + txn.set_durability(Durability::None).unwrap(); + let mut table = txn.open_multimap_table(table_def).unwrap(); + table.insert(0, i).unwrap(); + drop(table); + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + let bytes = txn.stats().unwrap().stored_bytes(); + assert!(bytes > last_size, "{i}"); + last_size = bytes; + } +} + +#[test] +fn no_downgrade_durability_with_savepoint() { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + + let mut tx = db.begin_write().unwrap(); + tx.persistent_savepoint().unwrap(); + assert!(matches!( + tx.set_durability(Durability::None), + Err(SetDurabilityError::PersistentSavepointModified) + )); + assert!(matches!(tx.set_durability(Durability::Immediate), Ok(()))); +} + +#[test] +fn no_savepoint_resurrection() { + let tmpfile = create_tempfile(); + + let db = Database::builder() + .set_cache_size(41178283) + .create(tmpfile.path()) + .unwrap(); + + let tx = db.begin_write().unwrap(); + let persistent_savepoint = tx.persistent_savepoint().unwrap(); + tx.commit().unwrap(); + + let tx = db.begin_write().unwrap(); + let savepoint2 = tx.ephemeral_savepoint().unwrap(); + tx.delete_persistent_savepoint(persistent_savepoint) + .unwrap(); + tx.commit().unwrap(); + + let mut tx = db.begin_write().unwrap(); + tx.restore_savepoint(&savepoint2).unwrap(); + tx.delete_persistent_savepoint(persistent_savepoint) + .unwrap(); + tx.commit().unwrap(); +} + +#[test] +fn non_durable_read_isolation() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let mut write_txn = db.begin_write().unwrap(); + write_txn.set_durability(Durability::None).unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + table.insert("hello", "world").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let read_table = read_txn.open_table(STR_TABLE).unwrap(); + assert_eq!("world", read_table.get("hello").unwrap().unwrap().value()); + + let mut write_txn = db.begin_write().unwrap(); + write_txn.set_durability(Durability::None).unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + table.remove("hello").unwrap(); + table.insert("hello2", "world2").unwrap(); + table.insert("hello3", "world3").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn2 = db.begin_read().unwrap(); + let read_table2 = read_txn2.open_table(STR_TABLE).unwrap(); + assert!(read_table2.get("hello").unwrap().is_none()); + assert_eq!( + "world2", + read_table2.get("hello2").unwrap().unwrap().value() + ); + assert_eq!( + "world3", + read_table2.get("hello3").unwrap().unwrap().value() + ); + assert_eq!(read_table2.len().unwrap(), 2); + + assert_eq!("world", read_table.get("hello").unwrap().unwrap().value()); + assert!(read_table.get("hello2").unwrap().is_none()); + assert!(read_table.get("hello3").unwrap().is_none()); + assert_eq!(read_table.len().unwrap(), 1); +} + +#[test] +fn range_query() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(U64_TABLE).unwrap(); + for i in 0..10 { + table.insert(&i, &i).unwrap(); + } + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(U64_TABLE).unwrap(); + let mut iter = table.range(3..7).unwrap(); + for i in 3..7u64 { + let (key, value) = iter.next().unwrap().unwrap(); + assert_eq!(i, key.value()); + assert_eq!(i, value.value()); + } + assert!(iter.next().is_none()); + + let mut iter = table.range(3..=7).unwrap(); + for i in 3..=7u64 { + let (key, value) = iter.next().unwrap().unwrap(); + assert_eq!(i, key.value()); + assert_eq!(i, value.value()); + } + assert!(iter.next().is_none()); + + let total: u64 = table + .range(1..=3) + .unwrap() + .map(|item| item.unwrap().1.value()) + .sum(); + assert_eq!(total, 6); +} + +#[test] +fn range_query_reversed() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(U64_TABLE).unwrap(); + for i in 0..10 { + table.insert(&i, &i).unwrap(); + } + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(U64_TABLE).unwrap(); + let mut iter = table.range(3..7).unwrap().rev(); + for i in (3..7u64).rev() { + let (key, value) = iter.next().unwrap().unwrap(); + assert_eq!(i, key.value()); + assert_eq!(i, value.value()); + } + assert!(iter.next().is_none()); + + // Test reversing multiple times + let mut iter = table.range(3..7).unwrap(); + let (key, _) = iter.next().unwrap().unwrap(); + assert_eq!(3, key.value()); + + let mut iter = iter.rev(); + let (key, _) = iter.next().unwrap().unwrap(); + assert_eq!(6, key.value()); + let (key, _) = iter.next().unwrap().unwrap(); + assert_eq!(5, key.value()); + + let mut iter = iter.rev(); + let (key, _) = iter.next().unwrap().unwrap(); + assert_eq!(4, key.value()); + + assert!(iter.next().is_none()); +} + +#[test] +fn alias_table() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let write_txn = db.begin_write().unwrap(); + let table = write_txn.open_table(STR_TABLE).unwrap(); + let result = write_txn.open_table(STR_TABLE); + assert!(matches!( + result.err().unwrap(), + TableError::TableAlreadyOpen(_, _) + )); + drop(table); +} + +#[test] +fn delete_table() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let y_def: MultimapTableDefinition<&str, &str> = MultimapTableDefinition::new("y"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + table.insert("hello", "world").unwrap(); + let mut multitable = write_txn.open_multimap_table(y_def).unwrap(); + multitable.insert("hello2", "world2").unwrap(); + } + write_txn.commit().unwrap(); + + let write_txn = db.begin_write().unwrap(); + assert!(write_txn.delete_table(STR_TABLE).unwrap()); + assert!(!write_txn.delete_table(STR_TABLE).unwrap()); + assert!(write_txn.delete_multimap_table(y_def).unwrap()); + assert!(!write_txn.delete_multimap_table(y_def).unwrap()); + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let result = read_txn.open_table(STR_TABLE); + assert!(result.is_err()); + let result = read_txn.open_multimap_table(y_def); + assert!(result.is_err()); +} + +#[test] +fn delete_all_tables() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let x_def: TableDefinition<&str, &str> = TableDefinition::new("x"); + let y_def: TableDefinition<&str, &str> = TableDefinition::new("y"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(x_def).unwrap(); + table.insert("hello", "world").unwrap(); + let mut table = write_txn.open_table(y_def).unwrap(); + table.insert("hello", "world").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + assert_eq!(2, read_txn.list_tables().unwrap().count()); + + let write_txn = db.begin_write().unwrap(); + for table in write_txn.list_tables().unwrap() { + write_txn.delete_table(table).unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + assert_eq!(0, read_txn.list_tables().unwrap().count()); +} + +#[test] +fn dropped_write() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(STR_TABLE).unwrap(); + table.insert("hello", "world").unwrap(); + } + drop(write_txn); + let read_txn = db.begin_read().unwrap(); + let result = read_txn.open_table(STR_TABLE); + assert!(matches!(result, Err(TableError::TableDoesNotExist(_)))); +} + +#[test] +fn non_page_size_multiple() { + let tmpfile = create_tempfile(); + + let db = Database::create(tmpfile.path()).unwrap(); + let txn = db.begin_write().unwrap(); + let key = vec![0u8; 1024]; + let value = vec![0u8; 1]; + { + let mut table = txn.open_table(SLICE_TABLE).unwrap(); + table.insert(key.as_slice(), value.as_slice()).unwrap(); + } + txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(SLICE_TABLE).unwrap(); + assert_eq!(table.len().unwrap(), 1); +} + +#[test] +fn does_not_exist() { + let tmpfile = create_tempfile(); + fs::remove_file(tmpfile.path()).unwrap(); + let result = Database::open(tmpfile.path()); + if let Err(DatabaseError::Storage(StorageError::Io(e))) = result { + assert!(matches!(e.kind(), ErrorKind::NotFound)); + } else { + panic!(); + } + + let tmpfile = create_tempfile(); + + let result = Database::open(tmpfile.path()); + if let Err(DatabaseError::Storage(StorageError::Io(e))) = result { + assert!(matches!(e.kind(), ErrorKind::InvalidData)); + } else { + panic!(); + } +} + +#[test] +fn invalid_database_file() { + let mut tmpfile = create_tempfile(); + tmpfile.write_all(b"hi").unwrap(); + let result = Database::open(tmpfile.path()); + if let Err(DatabaseError::Storage(StorageError::Io(e))) = result { + assert!(matches!(e.kind(), ErrorKind::InvalidData)); + } else { + panic!(); + } + + let result = Database::create(tmpfile.path()); + if let Err(DatabaseError::Storage(StorageError::Io(e))) = result { + assert!(matches!(e.kind(), ErrorKind::InvalidData)); + } else { + panic!(); + } +} + +#[test] +fn wrong_types() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: TableDefinition = TableDefinition::new("x"); + let wrong_definition: TableDefinition = TableDefinition::new("x"); + + let txn = db.begin_write().unwrap(); + txn.open_table(definition).unwrap(); + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + assert!(matches!( + txn.open_table(wrong_definition), + Err(TableError::TableTypeMismatch { .. }) + )); + txn.abort().unwrap(); + + let txn = db.begin_read().unwrap(); + txn.open_table(definition).unwrap(); + assert!(matches!( + txn.open_table(wrong_definition), + Err(TableError::TableTypeMismatch { .. }) + )); +} + +#[test] +fn tree_balance() { + const EXPECTED_ORDER: usize = 9; + fn expected_height(mut elements: usize) -> u32 { + // Root may have only 2 entries + let mut height = 1; + elements /= 2; + + // Leaves may have only a single entry + height += 1; + + // Each internal node half-full, plus 1 to round up + height += (elements as f32).log((EXPECTED_ORDER / 2) as f32) as usize + 1; + + height.try_into().unwrap() + } + + let tmpfile = create_tempfile(); + + // One for the last table id counter, and one for the "x" -> TableDefinition entry + let num_internal_entries = 2; + + // Pages are 4kb, so use a key size such that 9 keys will fit + let key_size = 410; + let db = Database::builder().create(tmpfile.path()).unwrap(); + let txn = db.begin_write().unwrap(); + + let elements = (EXPECTED_ORDER / 2).pow(2) - num_internal_entries; + + { + let mut table = txn.open_table(SLICE_TABLE).unwrap(); + for i in (0..elements).rev() { + let mut key = vec![0u8; key_size]; + key[0..8].copy_from_slice(&(i as u64).to_le_bytes()); + table.insert(key.as_slice(), b"".as_slice()).unwrap(); + } + } + txn.commit().unwrap(); + + let expected = expected_height(elements + num_internal_entries); + let txn = db.begin_write().unwrap(); + let height = txn.stats().unwrap().tree_height(); + assert!(height <= expected, "height={height} expected={expected}",); + + let reduce_to = EXPECTED_ORDER / 2 - num_internal_entries; + { + let mut table = txn.open_table(SLICE_TABLE).unwrap(); + for i in 0..(elements - reduce_to) { + let mut key = vec![0u8; key_size]; + key[0..8].copy_from_slice(&(i as u64).to_le_bytes()); + table.remove(key.as_slice()).unwrap(); + } + } + txn.commit().unwrap(); + + let expected = expected_height(reduce_to + num_internal_entries); + let txn = db.begin_write().unwrap(); + let height = txn.stats().unwrap().tree_height(); + txn.abort().unwrap(); + assert!(height <= expected, "height={height} expected={expected}",); +} + +#[cfg(not(target_os = "wasi"))] +#[test] +fn database_lock() { + let tmpfile = create_tempfile(); + let result = Database::create(tmpfile.path()); + assert!(result.is_ok()); + let result2 = Database::open(tmpfile.path()); + assert!( + matches!(result2, Err(DatabaseError::DatabaseAlreadyOpen)), + "{result2:?}", + ); + drop(result); + let result = Database::open(tmpfile.path()); + assert!(result.is_ok()); +} + +#[test] +fn persistent_savepoint() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let definition: TableDefinition = TableDefinition::new("x"); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(definition).unwrap(); + table.insert(&0, "hello").unwrap(); + } + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + let savepoint_id = txn.persistent_savepoint().unwrap(); + { + let mut table = txn.open_table(definition).unwrap(); + table.remove(&0).unwrap(); + } + txn.commit().unwrap(); + + drop(db); + let db = Database::create(tmpfile.path()).unwrap(); + // Make sure running the GC doesn't invalidate the savepoint + let txn = db.begin_write().unwrap(); + txn.commit().unwrap(); + let txn = db.begin_write().unwrap(); + txn.commit().unwrap(); + + let mut txn = db.begin_write().unwrap(); + let savepoint = txn.get_persistent_savepoint(savepoint_id).unwrap(); + + txn.restore_savepoint(&savepoint).unwrap(); + txn.commit().unwrap(); + + let txn = db.begin_read().unwrap(); + let table = txn.open_table(definition).unwrap(); + assert_eq!(table.get(&0).unwrap().unwrap().value(), "hello"); +} + +#[test] +fn savepoint() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let definition: TableDefinition = TableDefinition::new("x"); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(definition).unwrap(); + table.insert(&0, "hello").unwrap(); + } + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + let savepoint = txn.ephemeral_savepoint().unwrap(); + { + let mut table = txn.open_table(definition).unwrap(); + table.remove(&0).unwrap(); + } + txn.commit().unwrap(); + + let mut txn = db.begin_write().unwrap(); + let savepoint2 = txn.ephemeral_savepoint().unwrap(); + + txn.restore_savepoint(&savepoint).unwrap(); + + assert!(matches!( + txn.restore_savepoint(&savepoint2).err().unwrap(), + SavepointError::InvalidSavepoint + )); + txn.commit().unwrap(); + + let txn = db.begin_read().unwrap(); + let table = txn.open_table(definition).unwrap(); + assert_eq!(table.get(&0).unwrap().unwrap().value(), "hello"); + + // Test that savepoints can be used multiple times + let mut txn = db.begin_write().unwrap(); + txn.restore_savepoint(&savepoint).unwrap(); + txn.commit().unwrap(); +} + +// Regression test: restore_savepoint() does not clear pending_table_updates in the +// TableTreeMut. When a table is opened, modified, and closed (dropped) before +// restore_savepoint(), the modification is staged in pending_table_updates. On commit, +// flush_table_root_updates() re-applies these stale updates, effectively undoing the +// savepoint restore and causing data loss. +#[test] +fn savepoint_restore_data_loss_pending_table_updates() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let definition: TableDefinition = TableDefinition::new("data_loss_test"); + + // Step 1: Insert initial data and commit durably + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(definition).unwrap(); + table.insert(&1, "original").unwrap(); + } + txn.commit().unwrap(); + + // Step 2: In a new write transaction, create a savepoint BEFORE any modifications + let txn = db.begin_write().unwrap(); + let savepoint = txn.ephemeral_savepoint().unwrap(); + + // Step 3: Open table, insert new data, then close (drop) the table handle. + // Dropping the table handle calls close_table() which stages the modified root + // into pending_table_updates via stage_update_table_root(). + { + let mut table = txn.open_table(definition).unwrap(); + table.insert(&2, "should_be_rolled_back").unwrap(); + table.insert(&3, "also_should_be_rolled_back").unwrap(); + } + + // Step 4: Restore the savepoint. This restores the table tree root to the + // pre-modification state, BUT does not clear pending_table_updates. + let mut txn = txn; + txn.restore_savepoint(&savepoint).unwrap(); + + // Step 5: Commit. During commit, flush_table_root_updates() drains + // pending_table_updates and re-applies the stale modification from Step 3, + // effectively undoing the savepoint restore. + txn.commit().unwrap(); + + // Step 6: Verify. After restoring the savepoint, keys 2 and 3 should NOT exist. + // Only key 1 ("original") should be present. + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(definition).unwrap(); + + // Key 1 should still exist (it was committed before the savepoint) + assert_eq!(table.get(&1).unwrap().unwrap().value(), "original"); + + // BUG: Keys 2 and 3 should NOT exist after savepoint restore, but they do + // because pending_table_updates re-applied the stale modification during commit. + // When the bug is fixed, these assertions will hold: + assert!( + table.get(&2).unwrap().is_none(), + "DATA LOSS BUG: key 2 should not exist after savepoint restore, \ + but pending_table_updates re-applied stale modifications" + ); + assert!( + table.get(&3).unwrap().is_none(), + "DATA LOSS BUG: key 3 should not exist after savepoint restore, \ + but pending_table_updates re-applied stale modifications" + ); + + // Also verify the table length: should be 1, not 3 + assert_eq!( + table.len().unwrap(), + 1, + "DATA LOSS BUG: table should have 1 entry after savepoint restore, \ + but has {} due to stale pending_table_updates", + table.len().unwrap() + ); +} + +#[test] +fn compaction() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let definition: TableDefinition = TableDefinition::new("x"); + + let big_value = vec![0u8; 100 * 1024]; + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(definition).unwrap(); + // Insert 10MiB of data + for i in 0..100 { + table.insert(&i, big_value.as_slice()).unwrap(); + } + } + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(definition).unwrap(); + // Delete 90% of it + for i in 0..90 { + table.remove(&i).unwrap(); + } + } + txn.commit().unwrap(); + // Second commit to trigger dynamic compaction + let txn = db.begin_write().unwrap(); + txn.commit().unwrap(); + + // The values are > 1 page, so shouldn't get relocated. Therefore there should be a bunch of fragmented space, + // since we left the last 100 values in the db. + drop(db); + let file_size = tmpfile.as_file().metadata().unwrap().len(); + let mut db = Database::open(tmpfile.path()).unwrap(); + + assert!(db.compact().unwrap()); + drop(db); + let file_size2 = tmpfile.as_file().metadata().unwrap().len(); + assert!(file_size2 < file_size); +} + +fn require_send(_: &T) {} +fn require_sync(_: &T) {} + +#[test] +fn is_send() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let definition: TableDefinition = TableDefinition::new("x"); + + let txn = db.begin_write().unwrap(); + { + let table = txn.open_table(definition).unwrap(); + require_send(&table); + require_sync(&txn); + } + txn.commit().unwrap(); + + let txn = db.begin_read().unwrap(); + let table = txn.open_table(definition).unwrap(); + require_sync(&table); + require_sync(&txn); +} + +struct DelegatingTable> { + inner: T, + _key: PhantomData, + _value: PhantomData, +} + +impl> ReadableTable + for DelegatingTable +{ + fn get<'a>( + &self, + key: impl Borrow>, + ) -> redb::Result>> { + self.inner.get(key) + } + + fn range<'a, KR>(&self, range: impl RangeBounds + 'a) -> redb::Result> + where + KR: Borrow> + 'a, + { + self.inner.range(range) + } + + fn first(&self) -> redb::Result, AccessGuard<'_, V>)>> { + self.inner.first() + } + + fn last(&self) -> redb::Result, AccessGuard<'_, V>)>> { + self.inner.last() + } +} + +impl> ReadableTableMetadata + for DelegatingTable +{ + fn stats(&self) -> redb::Result { + self.inner.stats() + } + + fn len(&self) -> redb::Result { + self.inner.len() + } +} + +struct DelegatingMultimapTable> { + inner: T, + _key: PhantomData, + _value: PhantomData, +} + +impl> ReadableMultimapTable + for DelegatingMultimapTable +{ + fn get<'a>(&self, key: impl Borrow>) -> redb::Result> { + self.inner.get(key) + } + + fn range<'a, KR>( + &self, + range: impl RangeBounds + 'a, + ) -> redb::Result> + where + KR: Borrow> + 'a, + { + self.inner.range(range) + } +} + +impl> ReadableTableMetadata + for DelegatingMultimapTable +{ + fn stats(&self) -> redb::Result { + self.inner.stats() + } + + fn len(&self) -> redb::Result { + self.inner.len() + } +} + +#[test] +fn custom_table_type() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let definition: TableDefinition = TableDefinition::new("x"); + let definition_multimap: MultimapTableDefinition = + MultimapTableDefinition::new("multi"); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(definition).unwrap(); + table.insert(0, "hello").unwrap(); + let mut table = txn.open_multimap_table(definition_multimap).unwrap(); + table.insert(1, "world").unwrap(); + } + txn.commit().unwrap(); + + let txn = db.begin_read().unwrap(); + let table = DelegatingTable { + inner: txn.open_table(definition).unwrap(), + _key: Default::default(), + _value: Default::default(), + }; + assert_eq!("hello", table.get(0).unwrap().unwrap().value()); + let table = DelegatingMultimapTable { + inner: txn.open_multimap_table(definition_multimap).unwrap(), + _key: Default::default(), + _value: Default::default(), + }; + assert_eq!( + "world", + table.get(1).unwrap().next().unwrap().unwrap().value() + ); + + let txn = db.begin_write().unwrap(); + let table = DelegatingTable { + inner: txn.open_table(definition).unwrap(), + _key: Default::default(), + _value: Default::default(), + }; + assert_eq!("hello", table.get(0).unwrap().unwrap().value()); + let table = DelegatingMultimapTable { + inner: txn.open_multimap_table(definition_multimap).unwrap(), + _key: Default::default(), + _value: Default::default(), + }; + assert_eq!( + "world", + table.get(1).unwrap().next().unwrap().unwrap().value() + ); +} + +// Regression test for a bug where renaming a table with pending (dirty) modifications +// caused database corruption due to unfinalized checksums. +// +// Previously, `rename_table` wrote the table definition (with a DEFERRED placeholder +// checksum) into the master btree AND kept the identical root in `pending_table_updates`. +// During commit, `flush_table_root_updates()` compared the btree entry's root with the +// pending update's root, found them equal, and skipped checksum finalization. The fix +// adds an explicit dirty flag to pending updates so that checksum finalization is never +// skipped for tables with uncommitted pages. +#[test] +fn rename_table_with_modifications_data_loss() { + let tmpfile = create_tempfile(); + + const ORIGINAL_TABLE: TableDefinition = TableDefinition::new("original"); + const RENAMED_TABLE: TableDefinition = TableDefinition::new("renamed"); + + // Step 1: Create initial data in the "original" table and commit. + { + let db = Database::create(tmpfile.path()).unwrap(); + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(ORIGINAL_TABLE).unwrap(); + table.insert(0, "initial_value_0").unwrap(); + table.insert(1, "initial_value_1").unwrap(); + } + txn.commit().unwrap(); + } + + // Step 2: Modify the table and rename it in the same transaction. + { + let db = Database::create(tmpfile.path()).unwrap(); + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(ORIGINAL_TABLE).unwrap(); + table.insert(2, "new_value_2").unwrap(); + table.insert(3, "new_value_3").unwrap(); + txn.rename_table(table, RENAMED_TABLE).unwrap(); + } + txn.commit().unwrap(); + + // Verify data is accessible within the same session. + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(RENAMED_TABLE).unwrap(); + assert_eq!(table.get(0).unwrap().unwrap().value(), "initial_value_0"); + assert_eq!(table.get(1).unwrap().unwrap().value(), "initial_value_1"); + assert_eq!(table.get(2).unwrap().unwrap().value(), "new_value_2"); + assert_eq!(table.get(3).unwrap().unwrap().value(), "new_value_3"); + assert_eq!(table.len().unwrap(), 4); + } + + // Step 3: Reopen and verify data survives and integrity check passes. + { + let mut db = Database::builder().create(tmpfile.path()).unwrap(); + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(RENAMED_TABLE).unwrap(); + assert_eq!(table.get(0).unwrap().unwrap().value(), "initial_value_0"); + assert_eq!(table.get(1).unwrap().unwrap().value(), "initial_value_1"); + assert_eq!(table.get(2).unwrap().unwrap().value(), "new_value_2"); + assert_eq!(table.get(3).unwrap().unwrap().value(), "new_value_3"); + assert_eq!(table.len().unwrap(), 4); + drop(table); + drop(read_txn); + assert!(db.check_integrity().unwrap()); + } +} + +// Regression test: restore_savepoint() does not clear the freed_pages list in the +// TableNamespace. When a table is modified (triggering copy-on-write B-tree operations) +// and then the savepoint is restored, the old pages that were replaced during the +// modification remain in freed_pages even though the restored tree root still references +// them. On commit, these stale entries are stored in DATA_FREED_TABLE. In a subsequent +// transaction, process_freed_pages() frees these pages from the allocator, even though +// the committed tree still points to them. When those freed pages are reallocated and +// overwritten by new data, the original table's B-tree becomes corrupted, causing data +// loss. +#[test] +fn savepoint_restore_data_loss_stale_freed_pages() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let table_a: TableDefinition = TableDefinition::new("table_a"); + let table_b: TableDefinition = TableDefinition::new("table_b"); + + // Step 1: Insert initial data into table_a and commit durably. + // This creates committed B-tree pages (leaves + branches) for table_a. + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(table_a).unwrap(); + for i in 0..100u64 { + let value = vec![0u8; 200]; + table.insert(&i, value.as_slice()).unwrap(); + } + } + txn.commit().unwrap(); + + // Verify initial data is correct + { + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(table_a).unwrap(); + for i in 0..100u64 { + let val = table.get(&i).unwrap().unwrap(); + assert!(val.value().iter().all(|x| *x == 0)); + } + } + + // Step 2: In a new write transaction, create a savepoint, then modify all of + // table_a's data (triggering copy-on-write which adds old pages to freed_pages), + // then restore the savepoint and commit. + // + // BUG: restore_savepoint() resets the tree root but does NOT clear freed_pages. + // The old pages (which the restored root still references) remain in freed_pages. + // commit() stores them in DATA_FREED_TABLE, marking live pages for future freeing. + let txn = db.begin_write().unwrap(); + let savepoint = txn.ephemeral_savepoint().unwrap(); + + { + let mut table = txn.open_table(table_a).unwrap(); + // Modify every entry to force copy-on-write on all leaf pages. + // The old leaf pages go into freed_pages; new pages are allocated. + for i in 0..100u64 { + let overwrite = vec![0xFFu8; 200]; + table.insert(&i, overwrite.as_slice()).unwrap(); + } + } + + // Restore the savepoint: resets the tree root to its pre-modification state, + // frees the newly allocated pages, but leaves freed_pages untouched. + let mut txn = txn; + txn.restore_savepoint(&savepoint).unwrap(); + drop(savepoint); + + // Commit: flush_and_close() drains freed_pages (which still contains the stale old + // pages) and stores them in DATA_FREED_TABLE[current_txn]. The committed user root + // still references those pages. + txn.commit().unwrap(); + + // Step 3: Commit an empty transaction. During durable_commit(), + // process_freed_pages() processes DATA_FREED_TABLE entries from Step 2 and frees the + // stale pages from the allocator. The committed tree root still points to them. + let txn = db.begin_write().unwrap(); + txn.commit().unwrap(); + + // Step 4: Insert data into a DIFFERENT table (table_b) so that the allocator reuses + // the freed pages. This overwrites the old B-tree nodes of table_a with table_b data. + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(table_b).unwrap(); + for i in 0..300u64 { + let filler = vec![0xBBu8; 200]; + table.insert(&i, filler.as_slice()).unwrap(); + } + } + txn.commit().unwrap(); + + // Step 5: Read table_a. If the bug is present, the original pages have been freed + // and overwritten by table_b's data. Reading table_a's B-tree will encounter + // corrupted nodes, leading to wrong values, missing keys, or read errors. + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(table_a).unwrap(); + + let mut data_loss_detected = false; + for i in 0..100u64 { + match table.get(&i) { + Ok(Some(val)) => { + let v = val.value(); + if v.len() != 200 || v.iter().any(|x| *x != 0) { + // Value is present but corrupted + data_loss_detected = true; + break; + } + } + Ok(None) => { + // Key is missing entirely + data_loss_detected = true; + break; + } + Err(_) => { + // Storage error due to corrupted B-tree pages + data_loss_detected = true; + break; + } + } + } + + assert!( + !data_loss_detected, + "DATA LOSS: table_a data is corrupted after savepoint restore. \ + restore_savepoint() did not clear freed_pages, causing the old pages \ + (still referenced by the committed tree root) to be stored in DATA_FREED_TABLE \ + and later freed by process_freed_pages(). When those pages were reused by table_b, \ + table_a's B-tree became corrupted." + ); +} + +#[test] +fn restore_savepoint_from_foreign_database_panics() { + let tmpfile1 = create_tempfile(); + let tmpfile2 = create_tempfile(); + let db1 = Database::create(tmpfile1.path()).unwrap(); + let db2 = Database::create(tmpfile2.path()).unwrap(); + + let txn1 = db1.begin_write().unwrap(); + let foreign_savepoint = txn1.ephemeral_savepoint().unwrap(); + txn1.commit().unwrap(); + + let mut txn2 = db2.begin_write().unwrap(); + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + txn2.restore_savepoint(&foreign_savepoint) + })); + assert!(result.is_ok()); + assert!(matches!( + result.unwrap(), + Err(SavepointError::InvalidSavepoint) + )); +} + +#[test] +fn delete_table_panic_after_modification() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let table_a: TableDefinition = TableDefinition::new("table_a"); + + // Step 1: Insert several pages worth of committed data into table_a. + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(table_a).unwrap(); + for i in 0..100u64 { + table.insert(&i, &vec![0u8; 200][..]).unwrap(); + } + } + txn.commit().unwrap(); + + // Verify the data is present and durable before we attempt the destructive operation. + { + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(table_a).unwrap(); + assert_eq!(table.len().unwrap(), 100); + } + + // Step 2: In a single transaction, create an ephemeral savepoint (which enables allocation + // tracking via DATA_ALLOCATED_TABLE), modify entries in table_a to force copy-on-write + // (allocating new uncommitted pages), and then delete the table. + let txn = db.begin_write().unwrap(); + let _savepoint = txn.ephemeral_savepoint().unwrap(); + { + let mut table = txn.open_table(table_a).unwrap(); + for i in 0..100u64 { + table.insert(&i, &vec![0xFFu8; 200][..]).unwrap(); + } + } + let deleted = txn.delete_table(table_a).unwrap(); + assert!(deleted); + + let commit_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| txn.commit())); + assert!(commit_result.is_ok() && commit_result.as_ref().unwrap().is_ok()); +} + +// Regression test for an unbounded page leak when a persistent savepoint is created in +// a write transaction and then the transaction is aborted. +#[test] +fn persistent_savepoint_abort_unbounded_leak() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let table: TableDefinition = TableDefinition::new("data"); + + // Warm up the persistent-savepoint system tables so that later measurements + // reflect only the effect of the aborted savepoint, not one-time initialization. + { + let txn = db.begin_write().unwrap(); + let id = txn.persistent_savepoint().unwrap(); + txn.commit().unwrap(); + let txn = db.begin_write().unwrap(); + txn.delete_persistent_savepoint(id).unwrap(); + txn.commit().unwrap(); + } + + // Populate some data so that subsequent updates have something to copy-on-write. + { + let txn = db.begin_write().unwrap(); + { + let mut t = txn.open_table(table).unwrap(); + for i in 0..20u64 { + t.insert(i, i).unwrap(); + } + } + txn.commit().unwrap(); + } + + // Drain any pending freed pages so the baseline is stable. + for _ in 0..3 { + db.begin_write().unwrap().commit().unwrap(); + } + let txn = db.begin_write().unwrap(); + let baseline = txn.stats().unwrap().allocated_pages(); + txn.abort().unwrap(); + + // Repeat: create persistent savepoint, abort, do one modifying write, drain. + // Each iteration permanently leaks pages because the aborted savepoint's + // TransactionTracker state is never cleaned up. + const ITERATIONS: u64 = 20; + for round in 0..ITERATIONS { + // Create a persistent savepoint and abort the transaction. + { + let txn = db.begin_write().unwrap(); + let _id = txn.persistent_savepoint().unwrap(); + txn.abort().unwrap(); + } + + // Perform a modification. Its freed pages cannot be reclaimed because the + // ghost savepoint from the aborted transaction still pins an old + // oldest_live_read_transaction value. + { + let txn = db.begin_write().unwrap(); + { + let mut t = txn.open_table(table).unwrap(); + t.insert(0, round).unwrap(); + } + txn.commit().unwrap(); + } + + for _ in 0..3 { + db.begin_write().unwrap().commit().unwrap(); + } + } + + let txn = db.begin_write().unwrap(); + let after = txn.stats().unwrap().allocated_pages(); + txn.abort().unwrap(); + + assert_eq!( + baseline, + after, + "After {} iterations of persistent_savepoint+abort+modify, page usage grew \ + from {} to {} ({} pages leaked). Because the leak per iteration is \ + independent of N, running N iterations leaks O(N) pages.", + ITERATIONS, + baseline, + after, + after.saturating_sub(baseline), + ); +} + +#[test] +fn check_integrity_with_live_read_transaction() { + let tmpfile = create_tempfile(); + let mut db = Database::create(tmpfile.path()).unwrap(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(U64_TABLE).unwrap(); + for i in 0..10u64 { + table.insert(&i, &i).unwrap(); + } + } + txn.commit().unwrap(); + + // Hold a ReadTransaction, which keeps an Arc alive. + // Note: ReadTransaction does not borrow the Database, so we can still call + // `&mut self` methods on `db` while `read_txn` is alive. + let read_txn = db.begin_read().unwrap(); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| db.check_integrity())); + assert!(result.is_ok(), "check_integrity() should not panic"); + assert!(matches!( + result.unwrap(), + Err(DatabaseError::TransactionInProgress) + )); + + // After the transaction is dropped, check_integrity() should succeed. + drop(read_txn); + assert!(db.check_integrity().unwrap()); +} + +// Regression test: restore_savepoint() must not partially revert in-memory state +// when it cannot complete. +#[test] +fn restore_savepoint_partial_revert_commits_as_data_loss() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let tab: TableDefinition = TableDefinition::new("t"); + + // k1 at the state we will restore to later. + let txn = db.begin_write().unwrap(); + { + let mut t = txn.open_table(tab).unwrap(); + t.insert(1u64, 1u64).unwrap(); + } + txn.commit().unwrap(); + + // Persistent savepoint PS1 captures the {k1} state. + let txn = db.begin_write().unwrap(); + let ps1 = txn.persistent_savepoint().unwrap(); + txn.commit().unwrap(); + + // k2 (durably committed). + let txn = db.begin_write().unwrap(); + { + let mut t = txn.open_table(tab).unwrap(); + t.insert(2u64, 2u64).unwrap(); + } + txn.commit().unwrap(); + + // Persistent savepoint PS2 with id > PS1. Its existence is what makes the + // failed-restore path trigger. + let txn = db.begin_write().unwrap(); + let _ps2 = txn.persistent_savepoint().unwrap(); + txn.commit().unwrap(); + + // k3 (durably committed). + let txn = db.begin_write().unwrap(); + { + let mut t = txn.open_table(tab).unwrap(); + t.insert(3u64, 3u64).unwrap(); + } + txn.commit().unwrap(); + + // Sanity: all three keys are durable before we start. + { + let rt = db.begin_read().unwrap(); + let t = rt.open_table(tab).unwrap(); + assert_eq!(t.get(1u64).unwrap().unwrap().value(), 1); + assert_eq!(t.get(2u64).unwrap().unwrap().value(), 2); + assert_eq!(t.get(3u64).unwrap().unwrap().value(), 3); + } + + let mut txn = db.begin_write().unwrap(); + txn.set_durability(Durability::None).unwrap(); + let sp = txn.get_persistent_savepoint(ps1).unwrap(); + + let restore_result = txn.restore_savepoint(&sp); + assert!( + matches!( + restore_result, + Err(SavepointError::ImmediateDurabilityRequired) + ), + "restore_savepoint() should fail with ImmediateDurabilityRequired when \ + durability != Immediate and newer persistent savepoints exist, got: {restore_result:?}" + ); + + // A caller that ignores the restore error and commits anyway durably + // persists the partially-reverted state. + txn.commit().unwrap(); + + // If restore_savepoint() were transactional on failure, k2 and k3 would + // still be present. With the bug, the in-memory tree root was reverted by + // the failing restore_savepoint() call and then durably committed, so k2 + // and k3 are gone. + let rt = db.begin_read().unwrap(); + let t = rt.open_table(tab).unwrap(); + let k1 = t.get(1u64).unwrap().map(|v| v.value()); + let k2 = t.get(2u64).unwrap().map(|v| v.value()); + let k3 = t.get(3u64).unwrap().map(|v| v.value()); + assert_eq!(k1, Some(1)); + assert_eq!(k2, Some(2)); + assert_eq!(k3, Some(3)); +} + +// Regression test for a page leak when a transaction that attempts to +// restore_savepoint() an older savepoint is aborted instead of committed. +#[test] +fn restore_savepoint_abort_unbounded_leak() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let table: TableDefinition = TableDefinition::new("data"); + + // Warm up the persistent-savepoint system tables so the baseline is stable. + { + let txn = db.begin_write().unwrap(); + let id = txn.persistent_savepoint().unwrap(); + txn.commit().unwrap(); + let txn = db.begin_write().unwrap(); + txn.delete_persistent_savepoint(id).unwrap(); + txn.commit().unwrap(); + } + + // Populate some data so that later writes have something to copy-on-write. + { + let txn = db.begin_write().unwrap(); + { + let mut t = txn.open_table(table).unwrap(); + for i in 0..50u64 { + t.insert(i, i).unwrap(); + } + } + txn.commit().unwrap(); + } + + // Drain pending freed pages so the baseline is stable. + for _ in 0..3 { + db.begin_write().unwrap().commit().unwrap(); + } + let txn = db.begin_write().unwrap(); + let baseline = txn.stats().unwrap().allocated_pages(); + txn.abort().unwrap(); + + // Create an older persistent savepoint that will be restored (then aborted). + let older = { + let txn = db.begin_write().unwrap(); + let id = txn.persistent_savepoint().unwrap(); + txn.commit().unwrap(); + id + }; + + // One modifying write between the two savepoints, so the newer savepoint + // references a different tree root than the older one. + { + let txn = db.begin_write().unwrap(); + { + let mut t = txn.open_table(table).unwrap(); + t.insert(0, u64::MAX).unwrap(); + } + txn.commit().unwrap(); + } + + // The newer persistent savepoint: restore_savepoint(older) tries to + // invalidate this one, and the abort leaves it as a ghost. + let newer = { + let txn = db.begin_write().unwrap(); + let id = txn.persistent_savepoint().unwrap(); + txn.commit().unwrap(); + id + }; + + { + let mut txn = db.begin_write().unwrap(); + let sp = txn.get_persistent_savepoint(older).unwrap(); + txn.restore_savepoint(&sp).unwrap(); + drop(sp); + txn.abort().unwrap(); + } + + { + let txn = db.begin_write().unwrap(); + txn.delete_persistent_savepoint(older).unwrap(); + txn.commit().unwrap(); + } + + const ITERATIONS: u64 = 100; + for round in 0..ITERATIONS { + let txn = db.begin_write().unwrap(); + { + let mut t = txn.open_table(table).unwrap(); + t.insert(0, round).unwrap(); + } + txn.commit().unwrap(); + } + + drop(db); + let db = Database::create(tmpfile.path()).unwrap(); + + { + let mut txn = db.begin_write().unwrap(); + let sp = txn.get_persistent_savepoint(newer).unwrap(); + txn.restore_savepoint(&sp).unwrap(); + drop(sp); + txn.commit().unwrap(); + } + { + let txn = db.begin_write().unwrap(); + txn.delete_persistent_savepoint(newer).unwrap(); + txn.commit().unwrap(); + } + + // Drain pending freed pages so the final measurement is stable. + for _ in 0..3 { + db.begin_write().unwrap().commit().unwrap(); + } + let txn = db.begin_write().unwrap(); + let after = txn.stats().unwrap().allocated_pages(); + txn.abort().unwrap(); + + assert_eq!( + baseline, + after, + "After {} iterations of insert+commit between an aborted \ + restore_savepoint and the next restore_savepoint, page usage grew \ + from {} to {} ({} pages leaked). The leak per iteration is independent \ + of N, so running N iterations leaks O(N) pages.", + ITERATIONS, + baseline, + after, + after.saturating_sub(baseline), + ); +} + +#[test] +fn restore_savepoint_abort_after_ephemeral_drop() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let table: TableDefinition = TableDefinition::new("data"); + + { + let txn = db.begin_write().unwrap(); + { + let mut t = txn.open_table(table).unwrap(); + for i in 0..20u64 { + t.insert(i, i).unwrap(); + } + } + txn.commit().unwrap(); + } + + let older = { + let txn = db.begin_write().unwrap(); + let sp = txn.ephemeral_savepoint().unwrap(); + txn.commit().unwrap(); + sp + }; + + let newer = { + let txn = db.begin_write().unwrap(); + let sp = txn.ephemeral_savepoint().unwrap(); + txn.commit().unwrap(); + sp + }; + + { + let mut txn = db.begin_write().unwrap(); + txn.restore_savepoint(&older).unwrap(); + drop(newer); + txn.abort().unwrap(); + } + + { + let mut txn = db.begin_write().unwrap(); + txn.restore_savepoint(&older).unwrap(); + txn.commit().unwrap(); + } + drop(older); + for _ in 0..3 { + db.begin_write().unwrap().commit().unwrap(); + } + + assert!( + db.begin_read() + .unwrap() + .open_table(table) + .unwrap() + .get(&0u64) + .unwrap() + .is_some() + ); +} + +#[test] +fn extract_if_next_then_next_back_panic() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let table_def: TableDefinition = TableDefinition::new("t"); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(table_def).unwrap(); + table.insert(&1u64, &10u64).unwrap(); + } + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_table(table_def).unwrap(); + let mut iter = table.extract_if(|_, _| true).unwrap(); + + let first = iter.next(); + assert!(first.is_some()); + let first = first.unwrap(); + assert!(first.is_ok()); + + assert!(iter.next().is_none()); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| iter.next_back())); + assert!(result.is_ok()); + } + txn.abort().unwrap(); +} + +#[test] +fn multimap_value_next_back_does_not_update_len() { + const TABLE: MultimapTableDefinition = MultimapTableDefinition::new("m"); + + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let txn = db.begin_write().unwrap(); + { + let mut table = txn.open_multimap_table(TABLE).unwrap(); + for i in 0..5u32 { + table.insert(&1u32, &i).unwrap(); + } + } + txn.commit().unwrap(); + + let txn = db.begin_read().unwrap(); + let table = txn.open_multimap_table(TABLE).unwrap(); + let mut iter = table.get(&1u32).unwrap(); + assert_eq!(iter.len(), 5); + assert!(!iter.is_empty()); + + let _ = iter.next_back().unwrap().unwrap(); + assert_eq!(iter.len(), 4); + + for expected_remaining in (0..4).rev() { + iter.next_back().unwrap().unwrap(); + assert_eq!(iter.len(), expected_remaining as u64); + } + assert!(iter.is_empty()); + assert!(iter.next_back().is_none()); +} + +#[test] +#[should_panic(expected = "assertion failed: !name.is_empty()")] +fn table_definition_new_panics_on_empty_name() { + let name = String::new(); + let _def: TableDefinition = TableDefinition::new(&name); +} + +#[test] +#[should_panic(expected = "assertion failed: !name.is_empty()")] +fn multimap_table_definition_new_panics_on_empty_name() { + let name = String::new(); + let _def: MultimapTableDefinition = MultimapTableDefinition::new(&name); +} diff --git a/v2/redb/tests/multimap_tests.rs b/v2/redb/tests/multimap_tests.rs new file mode 100644 index 000000000..aae7fa07a --- /dev/null +++ b/v2/redb/tests/multimap_tests.rs @@ -0,0 +1,419 @@ +use redb::{ + Database, MultimapTableDefinition, ReadableDatabase, ReadableMultimapTable, + ReadableTableMetadata, TableError, +}; + +const STR_TABLE: MultimapTableDefinition<&str, &str> = MultimapTableDefinition::new("str_to_str"); +const SLICE_U64_TABLE: MultimapTableDefinition<&[u8], u64> = + MultimapTableDefinition::new("slice_to_u64"); +const U64_TABLE: MultimapTableDefinition = MultimapTableDefinition::new("u64"); + +fn create_tempfile() -> tempfile::NamedTempFile { + if cfg!(target_os = "wasi") { + tempfile::NamedTempFile::new_in("/tmp").unwrap() + } else { + tempfile::NamedTempFile::new().unwrap() + } +} + +fn get_vec( + table: &impl ReadableMultimapTable<&'static str, &'static str>, + key: &str, +) -> Vec { + let mut result = vec![]; + let mut iter = table.get(key).unwrap(); + loop { + let item = iter.next(); + if let Some(item_value) = item { + result.push(item_value.unwrap().value().to_string()); + } else { + return result; + } + } +} + +#[test] +fn len() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_multimap_table(STR_TABLE).unwrap(); + table.insert("hello", "world").unwrap(); + table.insert("hello", "world2").unwrap(); + table.insert("hi", "world").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_multimap_table(STR_TABLE).unwrap(); + assert_eq!(table.len().unwrap(), 3); + let untyped_table = read_txn.open_untyped_multimap_table(STR_TABLE).unwrap(); + assert_eq!(untyped_table.len().unwrap(), 3); +} + +#[test] +fn is_empty() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_multimap_table(STR_TABLE).unwrap(); + table.insert("hello", "world").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_multimap_table(STR_TABLE).unwrap(); + assert!(!table.is_empty().unwrap()); +} + +#[test] +fn insert() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_multimap_table(STR_TABLE).unwrap(); + assert!(!table.insert("hello", "world").unwrap()); + assert!(!table.insert("hello", "world2").unwrap()); + assert!(table.insert("hello", "world2").unwrap()); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_multimap_table(STR_TABLE).unwrap(); + assert_eq!( + vec!["world".to_string(), "world2".to_string()], + get_vec(&table, "hello") + ); + assert_eq!(table.len().unwrap(), 2); +} + +#[test] +fn range_query() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_multimap_table(SLICE_U64_TABLE).unwrap(); + for i in 0..5 { + table.insert(b"0".as_slice(), &i).unwrap(); + } + for i in 5..10 { + table.insert(b"1".as_slice(), &i).unwrap(); + } + for i in 10..15 { + table.insert(b"2".as_slice(), &i).unwrap(); + } + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_multimap_table(SLICE_U64_TABLE).unwrap(); + let start = b"0".as_ref(); + let end = b"1".as_ref(); + let mut iter = table.range(start..=end).unwrap(); + + { + let (key, mut values) = iter.next().unwrap().unwrap(); + for i in 0..5 { + assert_eq!(b"0", key.value()); + let value = values.next().unwrap().unwrap(); + assert_eq!(i, value.value()); + } + } + { + let (key, mut values) = iter.next().unwrap().unwrap(); + for i in 5..10 { + assert_eq!(b"1", key.value()); + let value = values.next().unwrap().unwrap(); + assert_eq!(i, value.value()); + } + } + assert!(iter.next().is_none()); + + let mut total: u64 = 0; + for item in table.range(start..=end).unwrap() { + let (_, values) = item.unwrap(); + total += values.map(|x| x.unwrap().value()).sum::(); + } + assert_eq!(total, 45); +} + +#[test] +fn range_lifetime() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: MultimapTableDefinition<&str, &str> = MultimapTableDefinition::new("x"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_multimap_table(definition).unwrap(); + table.insert("hello", "world").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_multimap_table(definition).unwrap(); + + let mut iter = { + let start = "hello".to_string(); + table.range::<&str>(start.as_str()..).unwrap() + }; + assert_eq!( + iter.next() + .unwrap() + .unwrap() + .1 + .next() + .unwrap() + .unwrap() + .value(), + "world" + ); + assert!(iter.next().is_none()); +} + +#[test] +fn range_arc_lifetime() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: MultimapTableDefinition<&str, &str> = MultimapTableDefinition::new("x"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_multimap_table(definition).unwrap(); + table.insert("hello", "world").unwrap(); + } + write_txn.commit().unwrap(); + + let mut iter = { + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_multimap_table(definition).unwrap(); + let start = "hello".to_string(); + table.range::<&str>(start.as_str()..).unwrap() + }; + assert_eq!( + iter.next() + .unwrap() + .unwrap() + .1 + .next() + .unwrap() + .unwrap() + .value(), + "world" + ); + assert!(iter.next().is_none()); +} + +#[test] +fn get_arc_lifetime() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: MultimapTableDefinition<&str, &str> = MultimapTableDefinition::new("x"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_multimap_table(definition).unwrap(); + table.insert("hello", "world").unwrap(); + } + write_txn.commit().unwrap(); + + let mut iter = { + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_multimap_table(definition).unwrap(); + let start = "hello".to_string(); + table.get(start.as_str()).unwrap() + }; + assert_eq!(iter.next().unwrap().unwrap().value(), "world"); + assert!(iter.next().is_none()); +} + +#[test] +fn delete() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_multimap_table(STR_TABLE).unwrap(); + table.insert("hello", "world").unwrap(); + table.insert("hello", "world2").unwrap(); + table.insert("hello", "world3").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_multimap_table(STR_TABLE).unwrap(); + assert_eq!(3, table.get("hello").unwrap().len()); + assert_eq!( + vec![ + "world".to_string(), + "world2".to_string(), + "world3".to_string() + ], + get_vec(&table, "hello") + ); + assert_eq!(table.len().unwrap(), 3); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_multimap_table(STR_TABLE).unwrap(); + table.remove("hello", "world2").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_multimap_table(STR_TABLE).unwrap(); + assert_eq!( + vec!["world".to_string(), "world3".to_string()], + get_vec(&table, "hello") + ); + assert_eq!(table.len().unwrap(), 2); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_multimap_table(STR_TABLE).unwrap(); + let mut iter = table.remove_all("hello").unwrap(); + assert_eq!("world", iter.next().unwrap().unwrap().value()); + assert_eq!("world3", iter.next().unwrap().unwrap().value()); + assert!(iter.next().is_none()); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_multimap_table(STR_TABLE).unwrap(); + assert!(table.is_empty().unwrap()); + let empty: Vec = vec![]; + assert_eq!(empty, get_vec(&table, "hello")); +} + +#[test] +fn wrong_types() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let definition: MultimapTableDefinition = MultimapTableDefinition::new("x"); + let wrong_definition: MultimapTableDefinition = MultimapTableDefinition::new("x"); + + let txn = db.begin_write().unwrap(); + txn.open_multimap_table(definition).unwrap(); + txn.commit().unwrap(); + + let txn = db.begin_write().unwrap(); + assert!(matches!( + txn.open_multimap_table(wrong_definition), + Err(TableError::TableTypeMismatch { .. }) + )); + txn.abort().unwrap(); + + let txn = db.begin_read().unwrap(); + txn.open_multimap_table(definition).unwrap(); + assert!(matches!( + txn.open_multimap_table(wrong_definition), + Err(TableError::TableTypeMismatch { .. }) + )); +} + +#[test] +fn efficient_storage() { + let tmpfile = create_tempfile(); + let expected_max_size = 1024 * 1024; + // Write enough values that big_key.len() * entries > db_size to check that duplicate key data is not stored + // and entries * sizeof(u32) > page_size to validate that large numbers of values can be stored per key + let entries = 10000; + let db = Database::create(tmpfile.path()).unwrap(); + let table_def: MultimapTableDefinition<&[u8], u32> = MultimapTableDefinition::new("x"); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_multimap_table(table_def).unwrap(); + let big_key = [0u8; 1000]; + for i in 0..entries { + table.insert(big_key.as_slice(), &i).unwrap(); + } + } + assert!(write_txn.stats().unwrap().stored_bytes() <= expected_max_size); + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_multimap_table(table_def).unwrap(); + assert_eq!(table.len().unwrap(), entries as u64); +} + +#[test] +fn reopen_table() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_multimap_table(STR_TABLE).unwrap(); + table.insert("0", "0").unwrap(); + } + { + let mut table = write_txn.open_multimap_table(STR_TABLE).unwrap(); + table.insert("1", "1").unwrap(); + } + write_txn.commit().unwrap(); +} + +#[test] +fn iter() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_multimap_table(U64_TABLE).unwrap(); + for i in 0..10 { + for j in 0..10 { + table.insert(&i, &j).unwrap(); + } + } + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_multimap_table(U64_TABLE).unwrap(); + let mut iter = table.iter().unwrap(); + for i in 0..10 { + let (k, mut values) = iter.next().unwrap().unwrap(); + assert_eq!(k.value(), i); + for j in 0..10 { + assert_eq!(values.next().unwrap().unwrap().value(), j); + } + } +} + +#[test] +fn multimap_signature_lifetimes() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + let def: MultimapTableDefinition<&str, u64> = MultimapTableDefinition::new("x"); + + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_multimap_table(def).unwrap(); + table.insert("bye", 0).unwrap(); + + let _ = { + let key = "hi".to_string(); + table.get(key.as_str()).unwrap() + }; + + let _ = { + let key = "hi".to_string(); + table.range(key.as_str()..).unwrap() + }; + + let _ = { + let key = "hi".to_string(); + table.remove_all(key.as_str()).unwrap() + }; + } + write_txn.commit().unwrap(); +} diff --git a/v2/redb/tests/multithreading_tests.rs b/v2/redb/tests/multithreading_tests.rs new file mode 100644 index 000000000..0329e7b95 --- /dev/null +++ b/v2/redb/tests/multithreading_tests.rs @@ -0,0 +1,115 @@ +#[cfg(not(target_os = "wasi"))] +mod multithreading_test { + use redb::{Database, ReadableDatabase, ReadableTable, ReadableTableMetadata, TableDefinition}; + use std::sync::Arc; + use std::thread; + + fn create_tempfile() -> tempfile::NamedTempFile { + if cfg!(target_os = "wasi") { + tempfile::NamedTempFile::new_in("/tmp").unwrap() + } else { + tempfile::NamedTempFile::new().unwrap() + } + } + + const TABLE: TableDefinition<&str, &str> = TableDefinition::new("x"); + #[test] + fn len() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + let db = Arc::new(db); + let write_txn = db.begin_write().unwrap(); + { + let mut table = write_txn.open_table(TABLE).unwrap(); + table.insert("hello", "world").unwrap(); + table.insert("hello2", "world2").unwrap(); + table.insert("hi", "world").unwrap(); + } + write_txn.commit().unwrap(); + + let db2 = db.clone(); + let t = thread::spawn(move || { + let read_txn = db2.begin_read().unwrap(); + let table = read_txn.open_table(TABLE).unwrap(); + assert_eq!(table.len().unwrap(), 3); + }); + t.join().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(TABLE).unwrap(); + assert_eq!(table.len().unwrap(), 3); + } + + #[test] + fn multithreaded_insert() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + const DEF1: TableDefinition<&str, &str> = TableDefinition::new("x"); + const DEF2: TableDefinition<&str, &str> = TableDefinition::new("y"); + let write_txn = db.begin_write().unwrap(); + { + let mut table1 = write_txn.open_table(DEF1).unwrap(); + let mut table2 = write_txn.open_table(DEF2).unwrap(); + + thread::scope(|s| { + s.spawn(|| { + table2.insert("hello", "world").unwrap(); + table2.insert("hello2", "world2").unwrap(); + }); + }); + + table1.insert("hello", "world").unwrap(); + table1.insert("hello2", "world2").unwrap(); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(DEF1).unwrap(); + assert_eq!(table.len().unwrap(), 2); + let table = read_txn.open_table(DEF2).unwrap(); + assert_eq!(table.len().unwrap(), 2); + } + + #[test] + fn multithreaded_re_read() { + let tmpfile = create_tempfile(); + let db = Database::create(tmpfile.path()).unwrap(); + + const DEF1: TableDefinition<&str, &str> = TableDefinition::new("x"); + const DEF2: TableDefinition<&str, &str> = TableDefinition::new("y"); + const DEF3: TableDefinition<&str, &str> = TableDefinition::new("z"); + let write_txn = db.begin_write().unwrap(); + { + let mut table1 = write_txn.open_table(DEF1).unwrap(); + let mut table2 = write_txn.open_table(DEF2).unwrap(); + let mut table3 = write_txn.open_table(DEF3).unwrap(); + table1.insert("hello", "world").unwrap(); + + thread::scope(|s| { + s.spawn(|| { + let value = table1.get("hello").unwrap().unwrap(); + table2.insert("hello2", value.value()).unwrap(); + }); + }); + thread::scope(|s| { + s.spawn(|| { + let value = table1.get("hello").unwrap().unwrap(); + table3.insert("hello2", value.value()).unwrap(); + }); + }); + + assert_eq!(table2.get("hello2").unwrap().unwrap().value(), "world"); + assert_eq!(table3.get("hello2").unwrap().unwrap().value(), "world"); + } + write_txn.commit().unwrap(); + + let read_txn = db.begin_read().unwrap(); + let table = read_txn.open_table(DEF1).unwrap(); + assert_eq!(table.len().unwrap(), 1); + let table = read_txn.open_table(DEF2).unwrap(); + assert_eq!(table.len().unwrap(), 1); + let table = read_txn.open_table(DEF3).unwrap(); + assert_eq!(table.len().unwrap(), 1); + } +} From bde61bdb7e9e9f58a4b99f422e7792cd7e0e15bc Mon Sep 17 00:00:00 2001 From: Lucid Programmer Date: Tue, 10 Nov 2026 19:22:43 +0530 Subject: [PATCH 2/2] contextdb: add v2 library and shell with typed columns, transactions, and concurrent access --- .github/workflows/check.yml | 58 ++ .github/workflows/ci.yml | 2 + v2/.config/nextest.toml | 9 + v2/.gitignore | 1 + v2/AGENTS.md | 112 +++ v2/Cargo.lock | 682 ++++++++++++++++ v2/Cargo.toml | 12 + v2/README.md | 62 ++ v2/clippy.toml | 10 + v2/contextdb-cli/Cargo.toml | 25 + v2/contextdb-cli/src/input.rs | 51 ++ v2/contextdb-cli/src/main.rs | 198 +++++ v2/contextdb-cli/src/output.rs | 260 +++++++ v2/contextdb-cli/src/shell.rs | 230 ++++++ v2/contextdb-cli/src/tests.rs | 37 + v2/contextdb-cli/tests/shell.rs | 732 ++++++++++++++++++ v2/contextdb/Cargo.toml | 20 + v2/contextdb/README.md | 251 ++++++ v2/contextdb/examples/measure.rs | 203 +++++ v2/contextdb/src/database/mod.rs | 551 +++++++++++++ v2/contextdb/src/database/tests.rs | 119 +++ v2/contextdb/src/lib.rs | 37 + v2/contextdb/src/memory/mod.rs | 206 +++++ v2/contextdb/src/memory/tests.rs | 85 ++ v2/contextdb/src/query/access.rs | 387 +++++++++ v2/contextdb/src/query/expression.rs | 318 ++++++++ v2/contextdb/src/query/mod.rs | 538 +++++++++++++ v2/contextdb/src/query/tests.rs | 131 ++++ v2/contextdb/src/read/mod.rs | 185 +++++ v2/contextdb/src/schema/check.rs | 220 ++++++ v2/contextdb/src/schema/mod.rs | 101 +++ v2/contextdb/src/schema/record.rs | 193 +++++ v2/contextdb/src/schema/render.rs | 100 +++ v2/contextdb/src/schema/tests.rs | 305 ++++++++ v2/contextdb/src/sql/ast.rs | 162 ++++ v2/contextdb/src/sql/grammar.pest | 157 ++++ v2/contextdb/src/sql/mod.rs | 29 + v2/contextdb/src/sql/parser/expr.rs | 243 ++++++ v2/contextdb/src/sql/parser/mod.rs | 463 +++++++++++ v2/contextdb/src/sql/parser/tests.rs | 264 +++++++ v2/contextdb/src/sql/plan.rs | 115 +++ v2/contextdb/src/sql/planner.rs | 122 +++ v2/contextdb/src/store/disk.rs | 348 +++++++++ v2/contextdb/src/store/folder.rs | 179 +++++ v2/contextdb/src/store/keys.rs | 113 +++ v2/contextdb/src/store/mod.rs | 699 +++++++++++++++++ v2/contextdb/src/store/record.rs | 147 ++++ v2/contextdb/src/store/tests.rs | 163 ++++ v2/contextdb/src/system/mod.rs | 51 ++ v2/contextdb/src/system/tests.rs | 19 + v2/contextdb/src/types/mod.rs | 390 ++++++++++ v2/contextdb/src/types/tests.rs | 28 + v2/contextdb/src/write/mod.rs | 433 +++++++++++ v2/contextdb/src/write/tests.rs | 127 +++ v2/contextdb/src/write/values.rs | 143 ++++ v2/contextdb/tests/clock.rs | 77 ++ v2/contextdb/tests/counted_work.rs | 190 +++++ v2/contextdb/tests/crash/main.rs | 403 ++++++++++ v2/contextdb/tests/memory.rs | 399 ++++++++++ v2/contextdb/tests/memory_ceiling.rs | 161 ++++ v2/contextdb/tests/readers.rs | 359 +++++++++ v2/contextdb/tests/refusals.rs | 196 +++++ v2/contextdb/tests/sql.rs | 149 ++++ v2/contextdb/tests/sql/aggregates.sql | 88 +++ v2/contextdb/tests/sql/comments.sql | 48 ++ v2/contextdb/tests/sql/create_table.sql | 60 ++ v2/contextdb/tests/sql/insert.sql | 61 ++ v2/contextdb/tests/sql/keys.sql | 112 +++ v2/contextdb/tests/sql/memory_limit.sql | 33 + v2/contextdb/tests/sql/order_limit.sql | 122 +++ v2/contextdb/tests/sql/refusals.sql | 37 + v2/contextdb/tests/sql/types.sql | 101 +++ v2/contextdb/tests/sql/update_delete.sql | 65 ++ v2/contextdb/tests/sql/where.sql | 155 ++++ v2/contextdb/tests/store.rs | 128 +++ v2/contextdb/tests/support/mod.rs | 523 +++++++++++++ v2/contextdb/tests/transactions.rs | 468 +++++++++++ v2/contextdb/tests/values.rs | 172 ++++ v2/numbers.sh | 67 ++ v2/redb/Cargo.lock | 28 +- v2/redb/Cargo.toml | 12 +- v2/redb/README.md | 34 + v2/redb/src/db.rs | 83 +- v2/redb/src/multimap_table.rs | 62 +- v2/redb/src/transaction_tracker.rs | 24 +- v2/redb/src/transactions.rs | 10 +- v2/redb/src/tree_store/btree.rs | 99 ++- v2/redb/src/tree_store/btree_base.rs | 62 +- v2/redb/src/tree_store/btree_iters.rs | 53 +- v2/redb/src/tree_store/btree_mutator.rs | 35 +- .../src/tree_store/page_store/cached_file.rs | 19 + .../src/tree_store/page_store/page_manager.rs | 126 ++- v2/redb/src/tree_store/table_tree.rs | 4 +- v2/redb/tests/contextdb_changes.rs | 370 +++++++++ 94 files changed, 15918 insertions(+), 103 deletions(-) create mode 100644 .github/workflows/check.yml create mode 100644 v2/.config/nextest.toml create mode 100644 v2/.gitignore create mode 100644 v2/AGENTS.md create mode 100644 v2/Cargo.lock create mode 100644 v2/Cargo.toml create mode 100644 v2/README.md create mode 100644 v2/clippy.toml create mode 100644 v2/contextdb-cli/Cargo.toml create mode 100644 v2/contextdb-cli/src/input.rs create mode 100644 v2/contextdb-cli/src/main.rs create mode 100644 v2/contextdb-cli/src/output.rs create mode 100644 v2/contextdb-cli/src/shell.rs create mode 100644 v2/contextdb-cli/src/tests.rs create mode 100644 v2/contextdb-cli/tests/shell.rs create mode 100644 v2/contextdb/Cargo.toml create mode 100644 v2/contextdb/README.md create mode 100644 v2/contextdb/examples/measure.rs create mode 100644 v2/contextdb/src/database/mod.rs create mode 100644 v2/contextdb/src/database/tests.rs create mode 100644 v2/contextdb/src/lib.rs create mode 100644 v2/contextdb/src/memory/mod.rs create mode 100644 v2/contextdb/src/memory/tests.rs create mode 100644 v2/contextdb/src/query/access.rs create mode 100644 v2/contextdb/src/query/expression.rs create mode 100644 v2/contextdb/src/query/mod.rs create mode 100644 v2/contextdb/src/query/tests.rs create mode 100644 v2/contextdb/src/read/mod.rs create mode 100644 v2/contextdb/src/schema/check.rs create mode 100644 v2/contextdb/src/schema/mod.rs create mode 100644 v2/contextdb/src/schema/record.rs create mode 100644 v2/contextdb/src/schema/render.rs create mode 100644 v2/contextdb/src/schema/tests.rs create mode 100644 v2/contextdb/src/sql/ast.rs create mode 100644 v2/contextdb/src/sql/grammar.pest create mode 100644 v2/contextdb/src/sql/mod.rs create mode 100644 v2/contextdb/src/sql/parser/expr.rs create mode 100644 v2/contextdb/src/sql/parser/mod.rs create mode 100644 v2/contextdb/src/sql/parser/tests.rs create mode 100644 v2/contextdb/src/sql/plan.rs create mode 100644 v2/contextdb/src/sql/planner.rs create mode 100644 v2/contextdb/src/store/disk.rs create mode 100644 v2/contextdb/src/store/folder.rs create mode 100644 v2/contextdb/src/store/keys.rs create mode 100644 v2/contextdb/src/store/mod.rs create mode 100644 v2/contextdb/src/store/record.rs create mode 100644 v2/contextdb/src/store/tests.rs create mode 100644 v2/contextdb/src/system/mod.rs create mode 100644 v2/contextdb/src/system/tests.rs create mode 100644 v2/contextdb/src/types/mod.rs create mode 100644 v2/contextdb/src/types/tests.rs create mode 100644 v2/contextdb/src/write/mod.rs create mode 100644 v2/contextdb/src/write/tests.rs create mode 100644 v2/contextdb/src/write/values.rs create mode 100644 v2/contextdb/tests/clock.rs create mode 100644 v2/contextdb/tests/counted_work.rs create mode 100644 v2/contextdb/tests/crash/main.rs create mode 100644 v2/contextdb/tests/memory.rs create mode 100644 v2/contextdb/tests/memory_ceiling.rs create mode 100644 v2/contextdb/tests/readers.rs create mode 100644 v2/contextdb/tests/refusals.rs create mode 100644 v2/contextdb/tests/sql.rs create mode 100644 v2/contextdb/tests/sql/aggregates.sql create mode 100644 v2/contextdb/tests/sql/comments.sql create mode 100644 v2/contextdb/tests/sql/create_table.sql create mode 100644 v2/contextdb/tests/sql/insert.sql create mode 100644 v2/contextdb/tests/sql/keys.sql create mode 100644 v2/contextdb/tests/sql/memory_limit.sql create mode 100644 v2/contextdb/tests/sql/order_limit.sql create mode 100644 v2/contextdb/tests/sql/refusals.sql create mode 100644 v2/contextdb/tests/sql/types.sql create mode 100644 v2/contextdb/tests/sql/update_delete.sql create mode 100644 v2/contextdb/tests/sql/where.sql create mode 100644 v2/contextdb/tests/store.rs create mode 100644 v2/contextdb/tests/support/mod.rs create mode 100644 v2/contextdb/tests/transactions.rs create mode 100644 v2/contextdb/tests/values.rs create mode 100755 v2/numbers.sh create mode 100644 v2/redb/tests/contextdb_changes.rs diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 000000000..194bf3575 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,58 @@ +name: Check + +# The check of the packages under v2/. Every step is one cargo command that runs as written from +# the repository root. +on: + push: + branches: [dev, main] + pull_request: + branches: [dev, main] + +permissions: + contents: read + +concurrency: + group: check-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + +jobs: + lint: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - run: cargo fmt --manifest-path v2/Cargo.toml --all --check + - run: cargo clippy --manifest-path v2/Cargo.toml --workspace --all-targets --locked -- -D warnings + - name: Shipped lines stay under 150,000 + run: test "$(find v2/contextdb/src v2/contextdb-cli/src \( -name '*.rs' -o -name '*.pest' \) ! -name tests.rs -exec cat {} + | wc -l)" -le 150000 + + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: taiki-e/install-action@cfdb7ed4ccc7c7dce37759eae89dc5674b469b39 # v2 + with: + tool: cargo-nextest@0.9.138 + - run: cargo nextest run --manifest-path v2/Cargo.toml --workspace --locked --no-tests=pass + - run: cargo test --manifest-path v2/Cargo.toml --workspace --locked --doc + - name: The storage library's own tests + run: cargo test --manifest-path v2/redb/Cargo.toml --locked --lib --tests + # A test is one the runner lists, or one script file. + - name: Tests stay under 1,000 + run: test "$(( $(cargo nextest list --manifest-path v2/Cargo.toml --workspace --locked -T oneline | wc -l) + $(find v2/contextdb/tests/sql -name '*.sql' | wc -l) ))" -le 1000 + + minimum-rust: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - run: rustup toolchain install 1.89 --profile minimal + - run: cargo +1.89 check --manifest-path v2/Cargo.toml --workspace --locked diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d337dade7..b7ee728d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,10 @@ name: CI on: push: branches: [dev, main] + paths-ignore: ['v2/**', '.github/workflows/check.yml'] pull_request: branches: [dev, main] + paths-ignore: ['v2/**', '.github/workflows/check.yml'] workflow_dispatch: permissions: diff --git a/v2/.config/nextest.toml b/v2/.config/nextest.toml new file mode 100644 index 000000000..ceaec6f4c --- /dev/null +++ b/v2/.config/nextest.toml @@ -0,0 +1,9 @@ +# A test that runs past its limit is stopped and fails by name. Nothing is retried. +[profile.default] +retries = 0 +slow-timeout = { period = "30s", terminate-after = 2 } + +# The million-row load is the one test that needs longer. +[[profile.default.overrides]] +filter = 'test(a_million_rows_load_and_scan_under_64_mib)' +slow-timeout = { period = "120s", terminate-after = 5 } diff --git a/v2/.gitignore b/v2/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/v2/.gitignore @@ -0,0 +1 @@ +/target diff --git a/v2/AGENTS.md b/v2/AGENTS.md new file mode 100644 index 000000000..b76f75fdd --- /dev/null +++ b/v2/AGENTS.md @@ -0,0 +1,112 @@ +# contextdb 2: working in `v2/` + +This workspace is contextdb 2: an embedded SQL database kept in one folder, the `contextdb` +program over it, and the storage library both are built on. Repository-wide policy (commit +messages, licence, contribution rules) is in the root [`AGENTS.md`](../AGENTS.md) and is not +repeated here; every command is in [`README.md`](README.md). + +## Packages and parts + +| Package | What it is | +|---|---| +| `contextdb/` | The library. Its public calls are the re-exports in `contextdb/src/lib.rs`; `contextdb/README.md` is also its documentation page, and the program in it is compiled as a documentation test. | +| `contextdb-cli/` | The `contextdb` program, a SQL shell. Every statement and command goes through the library's public calls; it never opens a store's files itself. | +| `redb/` | redb 4.1.0 with the changes listed at the top of `redb/README.md`. It keeps its own manifest and lock file and is not a workspace member. | + +The library is ten parts, one folder each under `contextdb/src/`: + +| Part | Does | Never | +|---|---|---| +| `types` | `Value`, the one `Error`, ids, table descriptions | holds behaviour: reads no store, checks no rule, decides no answer | +| `system` | the time of day, through `Clock`; the one place in the library that reads a clock | decides anything | +| `memory` | the budget and the reservations taken from it | allocates for another part: it counts what they are about to take and says no first | +| `sql` | grammar, parser, planner: text to a statement, a statement to a plan | touches storage or knows what a table holds | +| `store` | the folder and data file, how each record is keyed, views of one moment, the reader files (one in `readers/` per reader that is inside a statement, naming the commit it reads) and the locks | interprets SQL or a table's rules | +| `schema` | table declarations: checking, storing, printing them back | changes a row | +| `query` | answers a SELECT from one table; holds the one expression evaluator; counts the rows it reads | changes anything | +| `write` | applies INSERT, UPDATE and DELETE to a change set; checks types, NOT NULL, the primary key and UNIQUE rules; keeps index entries and row counts | returns a query's result to a caller | +| `database` | the handle a store is opened for writing with, its transactions, the one commit function | has a second way to commit | +| `read` | a session that reads a store without holding it for writing, from any process | changes anything a writer stored | + +## Where the parts meet + +A part reaches another only through what that part makes visible: the `pub(crate)` items of its +`mod.rs`, and for `sql` its `ast` and `plan` modules. The types handed across: + +| Type | Defined in | Who takes it | +|---|---|---| +| `Plan` (with `Effect`, `Expr`, `Statement`) | `sql` | `schema`, `write` and `query` each take a planned statement | +| `Read` (implemented by `View`, the store as of one commit, and by `ChangeSet`) | `store` | `query` and `write` read through it | +| `ChangeSet`, everything one transaction means to change, held in memory over a `View` | `store` | `schema` and `write` stage into it; `database` hands it to `Store::commit` | +| `Store`, `Key`, `Row`, `Cell`, `TableId`, `IndexId` | `store` | `database` and `read` open a `Store`; the rest name rows and indexes | +| `Budget`, `Reservation` | `memory` | everything whose memory grows with data holds a reservation | +| `Catalog`, `Table` | `schema` | `query`, `write`, `database` and `read` look tables up in it | +| `eval`, `matching` | `query` | `write` finds the rows of an UPDATE or DELETE with `query::matching` and works out values with `query::eval` | +| `QueryResult` | `query` | `database` and `read` build it and hand it to the caller | + +## Invariants and the tests that guard them + +Every path in this guide is written from `v2/`. + +| Invariant | Test | +|---|---| +| A commit is all or nothing under a kill at any storage call | `contextdb/tests/crash/main.rs`: `a_kill_at_any_storage_call_of_a_commit_leaves_all_or_nothing`, `a_kill_inside_a_commit_of_many_rows_keeps_rows_index_and_count_together` | +| What was committed is there after reopen | `contextdb/tests/store.rs`: `what_was_committed_is_there_after_close_and_reopen` | +| A commit that reached the disk is reported as committed | `contextdb/tests/crash/main.rs`: `a_commit_on_disk_is_reported_and_served_though_the_read_after_it_fails` | +| A reader in another process reads while the writer commits, and keeps answering when the writer is killed and restarted | `contextdb/tests/readers.rs`: `a_reader_in_another_process_reads_while_the_writer_commits`, `a_reader_keeps_answering_when_the_writer_is_killed_and_follows_the_restarted_writer` | +| A reader that reads the file header inside a commit gets the last whole commit | `redb/tests/contextdb_changes.rs`: `a_reader_that_catches_the_header_inside_a_commit_reads_the_last_whole_commit` | +| One writer; the second is refused at once | `contextdb/tests/store.rs`: `a_second_writer_is_refused_at_once_naming_the_holder_and_the_next_open_needs_no_retry` | +| The memory limit holds | `contextdb/tests/memory_ceiling.rs`: `a_million_rows_load_and_scan_under_64_mib` | +| A refused statement changes nothing | `contextdb/tests/memory.rs`: `a_statement_that_would_cross_the_limit_is_refused_by_name_and_committed_rows_stay`; `contextdb/tests/transactions.rs`: `a_refused_statement_changes_nothing_and_leaves_the_transaction_usable` | +| A damaged page is reported as damage | `contextdb/tests/crash/main.rs`: `a_damaged_page_is_refused_as_damage_by_name`; `redb/tests/contextdb_changes.rs`: `a_page_with_a_wrong_checksum_is_refused_when_read` | +| Counted work: a count reads no rows, a key lookup reads one | `contextdb/tests/counted_work.rs`: `count_of_a_whole_table_reads_no_rows`, `a_lookup_by_primary_key_reads_one_row` | + +## Where a change lives + +| Change | Folder | Test it needs | +|---|---|---| +| A new SQL statement or clause | `contextdb/src/sql/` (`grammar.pest`, `parser/`, the `Statement` type in `ast.rs`, `plan.rs`, `planner.rs`; take its first words out of the grammar's list of what a later version reads), then the part that runs it: `schema`, `write` or `query` | a script under `contextdb/tests/sql/` | +| A new refusal (error kind) | `Error` in `contextdb/src/types/mod.rs`; how the program prints it in `contextdb-cli/src/output.rs` | `contextdb/tests/refusals.rs` for its fields and that nothing changed; `contextdb/tests/sql/refusals.sql` for its name | +| What a statement returns | `contextdb/src/query/` for a SELECT, `contextdb/src/write/` for a row count | the expected output of the scripts under `contextdb/tests/sql/`, rewritten and the diff read | +| How a store is laid out on disk | `contextdb/src/store/` (keys, records, the data file, the folder's files) | `contextdb/tests/values.rs` and `contextdb/tests/store.rs` for reopen, `contextdb/tests/crash/` for a kill | +| The storage library | `redb/src/`; `redb/README.md` lists every change against upstream, so add yours there. The storage library decides which pages may be reused; the `store` part only tells it the oldest commit a reader still holds, and keeps the reader files and the lock | `redb/tests/contextdb_changes.rs` | +| A new shell option or dot command | `contextdb-cli/src/main.rs` for an option, `contextdb-cli/src/shell.rs` for a command | `contextdb-cli/tests/shell.rs` | +| A new public call | the part that owns it, re-exported in `contextdb/src/lib.rs`, described in `contextdb/README.md` | a Rust test under `contextdb/tests/` | + +## How tests are written here + +- A test goes through a public call of the library or through the built program, never an inner item. +- SQL behaviour is a script under `contextdb/tests/sql/` with its expected output; tests that kill a process and reopen the store live in `contextdb/tests/crash/`. +- A unit test of one pure function may sit beside it in a file named `tests.rs`. +- A test asserts states, values, error kinds and counts, never a duration, and waits only on something a process announces. +- Its data is fixed, and each test has its own store folder (`support::folder` in `contextdb/tests/support/mod.rs`). +- Nothing is retried (`.config/nextest.toml` sets `retries = 0`) and no test is ignored. + +Two lint settings hold this up: + +- `clippy.toml` refuses `std::thread::sleep`, starting a thread, `Instant::now`, `SystemTime::now` and `uuid::Uuid::new_v4`: the engine does not sleep, start a thread or draw a random number, and reads the time only through `Clock`. +- `Cargo.toml` denies `clippy::iter_over_hash_type` for both members: a hash map is walked in a different order in every process, so nothing is built by walking one. + +## Do not add + +- A second way to do one thing: there is one commit function, one expression evaluator, one `Error`. +- A test-only item in shipped code: no test-only name, feature or `pub` door; tests use what a user has. +- A check another part already makes: `write` checks a table's rules, `memory` says no, `store` only keeps. +- A lint switched off. Outside `redb/` there are four `allow` attributes and they stay the only ones: the machine clock in `contextdb/src/system/mod.rs` (the one place the time of day is read); the timer in `contextdb/examples/measure.rs` (measuring elapsed time is what that program is for); and two in `contextdb/tests/support/mod.rs`, one on its one thread (it moves a child's output lines to a channel so that a wait can give up) and `dead_code` for the whole file (each test program uses only some of its helpers). + +## Fast commands + +Every command is run from the repository's root folder, the one that contains `v2/`. The whole check, then one Rust test, then one SQL script: + +```sh +cargo fmt --manifest-path v2/Cargo.toml --all --check +cargo clippy --manifest-path v2/Cargo.toml --workspace --all-targets --locked -- -D warnings +cargo nextest run --manifest-path v2/Cargo.toml --workspace --locked +cargo test --manifest-path v2/Cargo.toml --workspace --locked --doc +cargo test --manifest-path v2/redb/Cargo.toml --locked --lib --tests +cargo +1.89 check --manifest-path v2/Cargo.toml --workspace --locked + +cargo nextest run --manifest-path v2/Cargo.toml -p contextdb a_fresh_store_is_one_folder + +SQL_SCRIPT=keys.sql cargo test --manifest-path v2/Cargo.toml -p contextdb --test sql +``` diff --git a/v2/Cargo.lock b/v2/Cargo.lock new file mode 100644 index 000000000..94b653711 --- /dev/null +++ b/v2/Cargo.lock @@ -0,0 +1,682 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "ar_archive_writer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" +dependencies = [ + "object", +] + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cc" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ff2a8126fff0f8c839080d619675a585e61720bb12d2baceae45769fd66f155" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "clipboard-win" +version = "5.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052af7878466feccda12ba7a6a67d019d712f296809559b47c12c8cc196d3a9a" +dependencies = [ + "error-code", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "contextdb" +version = "2.0.0" +dependencies = [ + "contextdb-redb", + "pest", + "pest_derive", + "serde_json", + "uuid", +] + +[[package]] +name = "contextdb-cli" +version = "2.0.0" +dependencies = [ + "clap", + "contextdb", + "rustyline", + "serde_json", +] + +[[package]] +name = "contextdb-redb" +version = "4.1.0" +dependencies = [ + "libc", +] + +[[package]] +name = "error-code" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd1574e617a9a6c1f8f8181bd7e841e1f20e4277607ef28945765188d6284d4" + +[[package]] +name = "js-sys" +version = "0.3.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc09d2154098bbf4ef54eab6b117a3c24cbbffe94046b16ee6d6d4510584d3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pest" +version = "2.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec53fd1d256900a9a771bf2720d42cef6f8bc91b4e25065fbd291e3e7222576" +dependencies = [ + "memchr", + "pest_stack_guard", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf794fd301ddc20818a6b4de85ed507f4b45300245cec948982e69e12e3bd7" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3073afae5db334c25dea9fc3e230a4f191e920c1df080cb9c0b37c9ce1dbb41" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc7f614b3742541642af1685faaa910bf6333e3cd3729c2cccd118146a57327" +dependencies = [ + "pest", +] + +[[package]] +name = "pest_stack_guard" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac6154fd4418b8f0b82532c59c26f303f9ca59a64c883e8ce3971fd82733439" +dependencies = [ + "psm", + "stacker", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psm" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "rustyline" +version = "17.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e902948a25149d50edc1a8e0141aad50f54e22ba83ff988cf8f7c9ef07f50564" +dependencies = [ + "bitflags", + "cfg-if", + "clipboard-win", + "libc", + "log", + "memchr", + "nix", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "windows-sys 0.60.2", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae9ccbea3a7b0b09443036490c8f34a8b1b760fd345e5c0dae0aac718faea79" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca718471876992059f251e33aafc334346ada08042b7661dd1fe313a9bced557" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.29.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bed99139f7dac9b5cfe5f17be4d66dc7079a34dee1d74bcc77217119240e267c" +dependencies = [ + "getrandom", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0181c14ddd6c7b8b0b0132a5f1ea2f32be0d893cd3ea97b6e3b28886b8a3d1c" +dependencies = [ + "cfg-if", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e931a02a39abdbf66966fc35902b5ed58344750a70885bed4ebb3d8eaf28ed9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ca4b54280e455bd831e33a6b8e734baf432fe63fbc35c42ac14ce67a043a53a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1f9c3798725016c6ee5f2f57869ec0831e8bbc0ab0310a86e787e97f41389e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/v2/Cargo.toml b/v2/Cargo.toml new file mode 100644 index 000000000..727d0de02 --- /dev/null +++ b/v2/Cargo.toml @@ -0,0 +1,12 @@ +# The library, the program and the storage library of contextdb 2. They have their own lock file, +# and nothing outside this folder depends on them. Every command that checks or tests them is run +# from the repository root with `--manifest-path v2/Cargo.toml`; see README.md beside this file. +[workspace] +resolver = "3" +members = ["contextdb", "contextdb-cli"] +# The storage library keeps the manifest and the lock file it was published with. +exclude = ["redb"] + +# A hash map is walked in a different order in every process, so nothing is built by walking one. +[workspace.lints.clippy] +iter_over_hash_type = "deny" diff --git a/v2/README.md b/v2/README.md new file mode 100644 index 000000000..d0029a4f2 --- /dev/null +++ b/v2/README.md @@ -0,0 +1,62 @@ +# contextdb 2 + +This folder is contextdb 2: the library (`contextdb/`), the `contextdb` program +(`contextdb-cli/`) and the storage library both are built on (`redb/`, which is redb 4.1.0 with +a few changes listed in its own README). It is a Cargo workspace of its own, with its own lock +file. The packages under `crates/` are contextdb 1; nothing there depends on anything here, and +nothing here depends on anything there. What the library does is in +[`contextdb/README.md`](contextdb/README.md). + +Every command below is run from the repository root. Pull requests go to the `dev` branch, where +`.github/workflows/check.yml` runs the same commands (the test run with `--no-tests=pass` +added): the three test commands on Linux and macOS, and the format check, the lint and the +oldest-Rust check on Linux only. + +## The whole check + +Tests run under [cargo-nextest](https://nexte.st), which stops a test that hangs and names it; +install it once with `cargo install cargo-nextest --version 0.9.138 --locked`. + +```sh +cargo fmt --manifest-path v2/Cargo.toml --all --check +cargo clippy --manifest-path v2/Cargo.toml --workspace --all-targets --locked -- -D warnings +cargo nextest run --manifest-path v2/Cargo.toml --workspace --locked +cargo test --manifest-path v2/Cargo.toml --workspace --locked --doc +cargo test --manifest-path v2/redb/Cargo.toml --locked --lib --tests +cargo +1.89 check --manifest-path v2/Cargo.toml --workspace --locked +``` + +The last line checks the oldest Rust the packages declare (`rustup toolchain install 1.89` +first). The workflow also fails when the library and the program together pass 150,000 lines or +the tests pass 1,000; the two lines that count them are in the workflow file. + +## One test + +```sh +cargo nextest run --manifest-path v2/Cargo.toml -p contextdb a_fresh_store_is_one_folder +``` + +The Rust tests of the library are in `contextdb/tests/`, the ones that kill a process in +`contextdb/tests/crash/`, and the program's in `contextdb-cli/tests/`. A test uses only what a +user of the library or the program has. + +## One SQL script + +A script in `contextdb/tests/sql/` is statements with the output expected of each; the format is +described at the top of `contextdb/tests/sql.rs`. One test runs them all. To run one file: + +```sh +SQL_SCRIPT=keys.sql cargo test --manifest-path v2/Cargo.toml -p contextdb --test sql +``` + +To rewrite the expected output of the scripts from what the engine now answers, then read the +diff before committing it: + +```sh +UPDATE_EXPECTED=1 cargo test --manifest-path v2/Cargo.toml -p contextdb --test sql +``` + +## Measurements + +`v2/numbers.sh` prints the numbers in the library's README for the commit it is run at. It only +measures: it never passes or fails, and the check does not run it. diff --git a/v2/clippy.toml b/v2/clippy.toml new file mode 100644 index 000000000..a43b201b2 --- /dev/null +++ b/v2/clippy.toml @@ -0,0 +1,10 @@ +# The one lint list of this workspace: calls the library never makes, and the clock read that +# only its `system` module makes. +disallowed-methods = [ + { path = "std::thread::sleep", reason = "wait on something a process announces; nothing in the engine sleeps" }, + { path = "std::thread::spawn", reason = "the engine starts no thread" }, + { path = "std::thread::Builder::spawn", reason = "the engine starts no thread" }, + { path = "std::time::Instant::now", reason = "read the time through contextdb::Clock" }, + { path = "std::time::SystemTime::now", reason = "read the time through contextdb::Clock" }, + { path = "uuid::Uuid::new_v4", reason = "the engine draws no random number; a test uses fixed ids" }, +] diff --git a/v2/contextdb-cli/Cargo.toml b/v2/contextdb-cli/Cargo.toml new file mode 100644 index 000000000..929a62571 --- /dev/null +++ b/v2/contextdb-cli/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "contextdb-cli" +version = "2.0.0" +edition = "2024" +rust-version = "1.89" +license = "Apache-2.0" +description = "The contextdb program: a SQL shell for a contextdb store" +repository = "https://github.com/context-graph-ai/contextdb" + +[[bin]] +name = "contextdb" +path = "src/main.rs" + +[dependencies] +clap = { version = "4", features = ["derive"] } +contextdb = { version = "2.0.0", path = "../contextdb" } +# Line editing and recall at a terminal, without its history-file and key-binding features. +# Version 19 needs a newer compiler than this workspace builds with. +rustyline = { version = "17", default-features = false } + +[dev-dependencies] +serde_json = "1" + +[lints] +workspace = true diff --git a/v2/contextdb-cli/src/input.rs b/v2/contextdb-cli/src/input.rs new file mode 100644 index 000000000..c21a2a28c --- /dev/null +++ b/v2/contextdb-cli/src/input.rs @@ -0,0 +1,51 @@ +//! Finds where a SQL statement ends in the text typed or piped so far. + +/// What the text gathered so far holds. +#[derive(Debug, PartialEq, Eq)] +pub enum Scan { + /// A whole statement: the text up to this byte offset, which is just past its `;`. + Ends(usize), + /// The start of a statement that the next line continues. + Open, + /// Nothing but spaces and closed comments: no statement has begun. + Blank, +} + +/// Looks for the first statement in `text`, as `psql` and `sqlite3` do: a statement ends at the +/// first `;` that is not inside a quoted string, a quoted name, a `--` comment or a `/* */` +/// comment, however many lines it spans. +/// +/// A `/*` that is not closed yet is [`Scan::Open`]: the next line continues the comment. +pub fn scan(text: &str) -> Scan { + let mut chars = text.char_indices().peekable(); + let mut blank = true; + while let Some((at, c)) = chars.next() { + let next = chars.peek().map(|(_, next)| *next); + match c { + ';' => return Scan::Ends(at + 1), + '-' if next == Some('-') => { + chars.by_ref().find(|(_, c)| *c == '\n'); + } + '/' if next == Some('*') => { + chars.next(); + let mut star = false; + let closed = chars.by_ref().any(|(_, c)| { + let closes = star && c == '/'; + star = c == '*'; + closes + }); + if !closed { + return Scan::Open; + } + } + '\'' | '"' => { + blank = false; + // A quote written twice inside its own quotes closes and opens again, which + // reads the same as staying inside. + chars.by_ref().find(|(_, close)| *close == c); + } + _ => blank &= c.is_whitespace(), + } + } + if blank { Scan::Blank } else { Scan::Open } +} diff --git a/v2/contextdb-cli/src/main.rs b/v2/contextdb-cli/src/main.rs new file mode 100644 index 000000000..82603005b --- /dev/null +++ b/v2/contextdb-cli/src/main.rs @@ -0,0 +1,198 @@ +//! The contextdb program: a SQL shell for a store. `server` and the other names of commands +//! that arrive with later versions are reserved and refused; they are not commands it has. +//! +//! It never does what the library cannot: every statement and every command goes through the +//! public API of `contextdb`, and nothing here opens the store's files itself. + +mod input; +mod output; +mod shell; +#[cfg(test)] +mod tests; + +use std::path::PathBuf; +use std::process::ExitCode; + +use clap::Parser; +use contextdb::{Database, DatabaseOpenOptions, ReadSession, ReadSessionOptions}; + +use output::Output; +use shell::Store; + +/// The exit status of a run in which the store could not be opened or closed, or a piped +/// statement or shell command was refused. +const EXIT_FAILED: u8 = 1; +/// The exit status of a command line that asks for something the program cannot do. Nothing +/// was opened. The argument parser exits with the same status for a line it cannot read. +const EXIT_USAGE: u8 = 2; + +/// Commands that arrive with later versions. The word is theirs already, so a folder with one +/// of these names is opened by writing it as a path. +const LATER_COMMANDS: [&str; 6] = [ + "server", "snapshot", "inspect", "diagnose", "reset", "purge", +]; + +/// What `--help` says after the options. +const AFTER_HELP: &str = r#"INPUT: + Statements are read from standard input, each ending at its `;`. On a terminal the + shell prompts and edits lines; piped input gets no prompt. + A refused statement in piped input is reported on the error stream, the statements + after it still run, and the run exits 1. + With --write the store is held for writing until the input ends: other processes can + read it meanwhile, and a second --write is refused until then. + A transaction that BEGIN opened and no COMMIT ended before the input ended is rolled + back. + +EXIT STATUS: + 0 every piped statement and command ran; a session at a terminal ends with 0 + whatever was refused in it + 1 a piped statement or command was refused, or the store could not be opened or + closed (a session at a terminal too) + 2 the command line was refused; nothing was opened + +MEMORY: + --memory-limit is this session's ceiling, in bytes or with K, M or G. Without it a + --write session has the limit the store remembers and a read session has none. + The store keeps 8M for its page cache and 1M is set aside before that, so the + smallest limit that opens a store is 9M. + `SET MEMORY_LIMIT 512M;` (or NONE) declares the limit the store remembers; it needs + --write, and inside a transaction it still takes effect at once and ROLLBACK does not + undo it. `SHOW MEMORY_LIMIT;` reports this session's limit and what is in use. + +JSON (--json), one document a line: + a SELECT or SHOW MEMORY_LIMIT {"result":{"columns":[...],"rows":[{...}]}} + any other statement {"rows_affected":N} + .tables {"tables":{"items":[...]}} + .schema {"schema":{"table":...,"ddl":...}} + .help {"help":[...]} + a refusal {"error":{"class":...,"message":...,"detail":{"kind":...}}} + on the error stream; "detail" is left out when the + refusal is the shell's own (an unknown command, a + reserved name) + A command line the program cannot read is refused in plain text. + +RESERVED NAMES: + server, snapshot, inspect, diagnose, reset and purge are kept for commands of later + versions. A folder with one of these names is written as a path: ./server + +THE STORE: + The folder holds `data`, `lock` and `readers`; together they are the store. The + README of the contextdb library says what each is. + +EXAMPLES: + contextdb ./notes + Read a store that is there, also while another process writes it. Never creates a + store and never changes its data. + + contextdb ./notes --write + Open the store for writing, creating it if it is missing. + + echo "SELECT * FROM notes LIMIT 5;" | contextdb ./notes --json + Pipe statements in and read one JSON document per statement. + + In the shell: .help + List the shell's commands."#; + +/// A SQL shell for a contextdb store. +#[derive(Parser)] +#[command( + name = "contextdb", + version, + override_usage = "contextdb [OPTIONS] ", + after_help = AFTER_HELP +)] +struct Args { + /// The store's folder. A folder named like a command is written as a path: ./server + path: PathBuf, + + /// Open the store for writing, creating it if it is missing. Without this the shell + /// only reads. + #[arg(long)] + write: bool, + + /// Machine output: one JSON document per statement or command on the output, and every + /// refusal as a JSON document on the error stream. + #[arg(long)] + json: bool, + + /// The most memory the session may use for data, in bytes or with K, M or G (64M, 4G). + #[arg(long, value_name = "SIZE", value_parser = parse_size)] + memory_limit: Option, +} + +fn main() -> ExitCode { + let args = Args::parse(); + let output = Output { json: args.json }; + + if let Some(command) = LATER_COMMANDS + .iter() + .find(|command| args.path.as_os_str() == **command) + { + output.refusal( + "usage", + &format!( + "`{command}` is a command, and this version does not have it yet; to open a \ + folder with that name write ./{command}" + ), + ); + return ExitCode::from(EXIT_USAGE); + } + + let store = match open(&args) { + Ok(store) => store, + Err(error) => { + output.error(&error); + return ExitCode::from(EXIT_FAILED); + } + }; + let mut succeeded = shell::run(&store, output); + if let Store::Write(db) = &store + && let Err(error) = db.close() + { + output.error(&error); + succeeded = false; + } + if succeeded { + ExitCode::SUCCESS + } else { + ExitCode::from(EXIT_FAILED) + } +} + +/// Opens the store the way the command line asked: for writing, or for reading only. +fn open(args: &Args) -> contextdb::Result { + if args.write { + let mut options = DatabaseOpenOptions::default(); + options.memory_limit = args.memory_limit; + Database::open_with_options(&args.path, options).map(Store::Write) + } else { + let mut options = ReadSessionOptions::default(); + options.memory_limit = args.memory_limit; + ReadSession::open_with_options(&args.path, options).map(Store::Read) + } +} + +/// Reads a size in bytes: digits, then nothing, K, M or G as powers of 1024. +fn parse_size(text: &str) -> Result { + let digits_end = text + .find(|c: char| !c.is_ascii_digit()) + .unwrap_or(text.len()); + let (digits, suffix) = text.split_at(digits_end); + let number: usize = digits.parse().map_err(|_| { + format!("'{text}' is not a size: write digits, then K, M or G, or nothing for bytes") + })?; + let unit: usize = match suffix.to_ascii_uppercase().as_str() { + "" => 1, + "K" => 1 << 10, + "M" => 1 << 20, + "G" => 1 << 30, + other => { + return Err(format!( + "unknown size suffix '{other}': use K, M or G, or no suffix for bytes" + )); + } + }; + number + .checked_mul(unit) + .ok_or_else(|| format!("'{text}' is too large")) +} diff --git a/v2/contextdb-cli/src/output.rs b/v2/contextdb-cli/src/output.rs new file mode 100644 index 000000000..35870b3c4 --- /dev/null +++ b/v2/contextdb-cli/src/output.rs @@ -0,0 +1,260 @@ +//! Everything the shell prints: results on the output, refusals on the error stream, each as +//! text for a person or, under `--json`, as one JSON document a line. + +use contextdb::serde_json::{Map, Value as Json, json}; +use contextdb::{Error, QueryResult, Value}; + +/// How this run prints. +#[derive(Clone, Copy)] +pub struct Output { + pub json: bool, +} + +impl Output { + /// The result of one statement: the rows of a SELECT, or how many rows a write changed. + pub fn result(self, result: &QueryResult) { + if result.columns().is_empty() { + self.rows_affected(result.rows_affected()); + } else if self.json { + // A row is written key by key so that its keys come in the order of `columns`. + let mut rows = String::new(); + for row in result.rows() { + if !rows.is_empty() { + rows.push(','); + } + rows.push('{'); + for (at, (column, value)) in result.columns().iter().zip(row).enumerate() { + if at > 0 { + rows.push(','); + } + rows.push_str(&format!("{}:{}", json!(column), cell(value))); + } + rows.push('}'); + } + println!( + "{{\"result\":{{\"columns\":{},\"rows\":[{rows}]}}}}", + json!(result.columns()) + ); + } else { + print!("{}", table(result.columns(), result.rows())); + } + } + + /// A statement that returns no rows succeeded and changed this many. + pub fn rows_affected(self, rows: u64) { + if self.json { + println!("{}", json!({ "rows_affected": rows })); + } else { + println!("ok (rows_affected={rows})"); + } + } + + /// What `.tables` answers: the table names, one a line. + pub fn tables(self, names: &[String]) { + if self.json { + println!("{}", json!({ "tables": { "items": names } })); + } else { + for name in names { + println!("{name}"); + } + } + } + + /// What `.schema` answers: the table's declaration as SQL. + pub fn schema(self, table: &str, declaration: &str) { + if self.json { + println!( + "{}", + json!({ "schema": { "table": table, "ddl": declaration } }) + ); + } else { + println!("{}", declaration.trim_end()); + } + } + + /// What `.help` answers. + pub fn help(self, lines: &[&str]) { + if self.json { + println!("{}", json!({ "help": lines })); + } else { + for line in lines { + println!("{line}"); + } + } + } + + /// A refusal from the library. A program reads `class` and `detail.kind`; the message is + /// for a person. + pub fn error(self, error: &Error) { + let class = match error { + Error::StoreMissing { .. } + | Error::DatabaseLocked { .. } + | Error::StoreCorrupted { .. } + | Error::StoreIo { .. } + | Error::MemoryBudgetExceeded { .. } => "io", + _ => "sql", + }; + let mut detail = Map::new(); + detail.insert("kind".to_owned(), json!(kind(error))); + if let Error::MemoryBudgetExceeded { + requested_bytes, + available_bytes, + budget_limit_bytes, + .. + } = error + { + detail.insert("requested_bytes".to_owned(), json!(requested_bytes)); + detail.insert("available_bytes".to_owned(), json!(available_bytes)); + detail.insert("budget_limit_bytes".to_owned(), json!(budget_limit_bytes)); + } + // Where an option of this program is the way on, the sentence is the program's own: + // the library's does not know the options. + let message = match error { + Error::StoreMissing { path } => format!( + "there is no store at this path: {}; --write creates it", + path.display() + ), + Error::DatabaseLocked { holder_pid, path } => format!( + "process {holder_pid} holds the store at {} for writing; wait for it to \ + finish, or open without --write to read", + path.display() + ), + Error::ReadOnlySession => { + "this statement writes and this session only reads; rerun with --write".to_owned() + } + Error::MemoryBudgetExceeded { .. } => format!( + "{}; raise it with --memory-limit", + error.to_string().trim_end_matches('.') + ), + other => other.to_string(), + }; + self.refused(class, &message, Some(detail)); + } + + /// A refusal of the shell's own: `class` is `usage` for a command line or a shell command + /// it cannot act on, `sql` for a statement. + pub fn refusal(self, class: &str, message: &str) { + self.refused(class, message, None); + } + + /// A line that starts like a shell command and is not one. For a person it is a plain line + /// with no `Error:` before it. + pub fn unknown_command(self, line: &str) { + let message = format!("Unknown command: {line}. Type \\? for help."); + if self.json { + self.refused("usage", &message, None); + } else { + eprintln!("{message}"); + } + } + + fn refused(self, class: &str, message: &str, detail: Option>) { + if self.json { + let mut error = Map::new(); + error.insert("class".to_owned(), json!(class)); + error.insert("message".to_owned(), json!(message)); + if let Some(detail) = detail { + error.insert("detail".to_owned(), Json::Object(detail)); + } + eprintln!("{}", json!({ "error": error })); + } else { + eprintln!("Error: {message}"); + } + } +} + +/// The word a program reads for a refusal's kind: the three the earlier program published +/// under another word keep it, and every other kind is its own name in lower snake case. +fn kind(error: &Error) -> String { + match error { + Error::StoreMissing { .. } => "store_not_found".to_owned(), + Error::DatabaseLocked { .. } => "held_by_writer".to_owned(), + Error::ReadOnlySession => "write_requires_flag".to_owned(), + other => { + let mut word = String::new(); + for c in format!("{other:?}") + .chars() + .take_while(char::is_ascii_alphanumeric) + { + if c.is_ascii_uppercase() && !word.is_empty() { + word.push('_'); + } + word.push(c.to_ascii_lowercase()); + } + word + } + } +} + +/// One value as JSON. +fn cell(value: &Value) -> Json { + match value { + Value::Null => Json::Null, + Value::Bool(value) => json!(value), + Value::Int64(value) | Value::Timestamp(value) => json!(value), + Value::Float64(value) => json!(value), + Value::Text(value) => json!(value), + Value::Uuid(value) => json!(value.to_string()), + Value::Json(value) => value.clone(), + Value::Vector(value) => json!(value), + Value::TxId(value) => json!(value.0), + } +} + +/// One value as a person reads it in a table. +fn text(value: &Value) -> String { + match value { + Value::Null => "NULL".to_owned(), + Value::Bool(value) => value.to_string(), + Value::Int64(value) | Value::Timestamp(value) => value.to_string(), + Value::Float64(value) => value.to_string(), + Value::Text(value) => value.clone(), + Value::Uuid(value) => value.to_string(), + Value::Json(value) => value.to_string(), + Value::Vector(value) => format!("{value:?}"), + Value::TxId(value) => value.0.to_string(), + } +} + +/// The rows as a bordered table with a `(N rows)` line under it, `(1 row)` for one. With no rows only that line +/// is printed. +pub fn table(columns: &[String], rows: &[Vec]) -> String { + let footer = match rows.len() { + 1 => "(1 row)\n".to_owned(), + count => format!("({count} rows)\n"), + }; + if rows.is_empty() { + return footer; + } + let rows: Vec> = rows + .iter() + .map(|row| row.iter().map(text).collect()) + .collect(); + let mut widths: Vec = columns.iter().map(|name| name.chars().count()).collect(); + for row in &rows { + for (width, cell) in widths.iter_mut().zip(row) { + *width = (*width).max(cell.chars().count()); + } + } + + let mut border = String::new(); + for width in &widths { + border.push('+'); + border.push_str(&"-".repeat(width + 2)); + } + border.push_str("+\n"); + let line = |cells: &[String]| { + let mut line = String::from("|"); + for (cell, width) in cells.iter().zip(&widths) { + line.push_str(&format!(" {cell:; Run a SQL statement. It may span lines and ends at its `;`.", + "BEGIN; ... COMMIT; Run statements as one transaction; ROLLBACK; undoes them, except SET MEMORY_LIMIT. Needs --write.", + ".tables \\dt List the tables.", + ".schema
\\d
Show a table's declaration as SQL.", + ".help \\? Show this list.", + ".quit \\q End the session.", +]; + +struct Shell<'a> { + store: &'a Store, + output: Output, + /// The transaction `BEGIN` opened, until `COMMIT` or `ROLLBACK`. + transaction: Option>, + /// Lines of a statement whose `;` has not come yet. + pending: String, + /// Whether anything was refused. + refused: bool, +} + +/// Runs the shell over the standard input until it ends or `.quit`. +/// +/// Returns whether the run succeeded. A piped run succeeds only when every statement and every +/// shell command did; a session at a terminal showed each refusal as it happened and always +/// succeeds. +pub fn run(store: &Store, output: Output) -> bool { + let mut shell = Shell { + store, + output, + transaction: None, + pending: String::new(), + refused: false, + }; + if std::io::stdin().is_terminal() { + shell.at_a_terminal(); + return true; + } + for line in std::io::stdin().lock().lines() { + let Ok(line) = line else { break }; + if shell.line(&line).is_break() { + return !shell.refused; + } + } + // Piped input ends its last statement without a `;`. + let rest = std::mem::take(&mut shell.pending); + shell.statement(&rest); + !shell.refused +} + +impl<'a> Shell<'a> { + /// Reads lines a person types, with line editing and recall of the session's earlier + /// lines. Nothing of the session is kept after it ends. + fn at_a_terminal(&mut self) { + let (banner, prompt) = match self.store { + Store::Read(_) => ( + "read-only session — pass --write to mutate", + "contextdb(ro)> ", + ), + Store::Write(_) => ("write session — mutations are enabled", "contextdb> "), + }; + let mut editor = match DefaultEditor::new() { + Ok(editor) => editor, + Err(error) => { + self.output + .refusal("io", &format!("the terminal cannot be read: {error}")); + return; + } + }; + eprintln!("{banner}"); + loop { + let open = !self.pending.is_empty(); + match editor.readline(if open { " ...> " } else { prompt }) { + Ok(line) => { + if line.trim().is_empty() && !open { + continue; + } + // A line that cannot be kept for recall is still run. + let _ = editor.add_history_entry(line.trim()); + if self.line(&line).is_break() { + return; + } + } + // Ctrl-C drops what was typed and the session goes on. + Err(ReadlineError::Interrupted) => self.pending.clear(), + // Ctrl-D, or a terminal that went away. + Err(_) => break, + } + } + if !self.pending.is_empty() { + eprintln!("Discarding incomplete statement (no closing `;`)."); + } + } + + /// Takes one line of input. Breaks when the line ends the session. + fn line(&mut self, line: &str) -> ControlFlow<()> { + // A shell command is one line, and only where no statement is open: inside a statement + // a line that starts with a dot is that statement's text. + let command = line.trim(); + if self.pending.is_empty() && command.starts_with(['.', '\\']) { + return self.command(command); + } + self.pending.push_str(line); + self.pending.push('\n'); + loop { + match scan(&self.pending) { + Scan::Ends(end) => { + let statement: String = self.pending.drain(..end).collect(); + self.statement(&statement); + } + Scan::Open => break, + Scan::Blank => { + self.pending.clear(); + break; + } + } + } + ControlFlow::Continue(()) + } + + /// Runs one shell command. + fn command(&mut self, line: &str) -> ControlFlow<()> { + let mut words = line.split_whitespace(); + let answered = match (words.next(), words.next(), words.next()) { + (Some(".tables" | "\\dt"), None, _) => match self.store { + Store::Read(reader) => reader.table_names(), + Store::Write(db) => Ok(db.table_names()), + } + .map(|names| self.output.tables(&names)), + (Some(".schema" | "\\d"), Some(table), None) => match self.store { + Store::Read(reader) => reader.declaration(table), + Store::Write(db) => db.declaration(table), + } + .map(|declaration| self.output.schema(table, &declaration)), + (Some(".help" | "\\?"), None, _) => { + self.output.help(&HELP); + Ok(()) + } + (Some(".quit" | ".exit" | "\\q"), None, _) => return ControlFlow::Break(()), + _ => { + self.refused = true; + self.output.unknown_command(line); + Ok(()) + } + }; + if let Err(error) = answered { + self.refused = true; + self.output.error(&error); + } + ControlFlow::Continue(()) + } + + /// Runs one statement and prints its result or its refusal. + fn statement(&mut self, text: &str) { + let sql = text.trim(); + let sql = sql.strip_suffix(';').unwrap_or(sql).trim_end(); + if sql.is_empty() { + return; + } + let ran = if sql.eq_ignore_ascii_case("BEGIN") { + self.begin() + } else if sql.eq_ignore_ascii_case("COMMIT") { + self.end(Transaction::commit) + } else if sql.eq_ignore_ascii_case("ROLLBACK") { + self.end(|transaction| { + transaction.rollback(); + Ok(()) + }) + } else { + self.execute(sql).map(|result| self.output.result(&result)) + }; + if let Err(error) = ran { + self.refused = true; + self.output.error(&error); + } + } + + fn execute(&self, sql: &str) -> Result { + let params = HashMap::new(); + match (&self.transaction, self.store) { + (Some(transaction), _) => transaction.execute(sql, ¶ms), + (None, Store::Write(db)) => db.execute(sql, ¶ms), + (None, Store::Read(reader)) => reader.execute(sql, ¶ms), + } + } + + /// `BEGIN`. With a transaction open already it changes nothing and answers the same. + fn begin(&mut self) -> Result<()> { + let Store::Write(db) = self.store else { + return Err(Error::ReadOnlySession); + }; + if self.transaction.is_none() { + self.transaction = Some(db.transaction()?); + } + self.output.rows_affected(0); + Ok(()) + } + + /// `COMMIT` or `ROLLBACK`. With no transaction open it changes nothing and answers the + /// same. + fn end(&mut self, how: impl FnOnce(Transaction<'a>) -> Result<()>) -> Result<()> { + if let Store::Read(_) = self.store { + return Err(Error::ReadOnlySession); + } + if let Some(transaction) = self.transaction.take() { + how(transaction)?; + } + self.output.rows_affected(0); + Ok(()) + } +} diff --git a/v2/contextdb-cli/src/tests.rs b/v2/contextdb-cli/src/tests.rs new file mode 100644 index 000000000..3a54262b8 --- /dev/null +++ b/v2/contextdb-cli/src/tests.rs @@ -0,0 +1,37 @@ +use contextdb::Value; + +use crate::input::{Scan, scan}; +use crate::output::table; + +#[test] +fn a_statement_ends_at_the_first_semicolon_outside_quotes_and_comments() { + assert_eq!(scan("SELECT 1; SELECT 2;"), Scan::Ends(9)); + assert_eq!(scan("SELECT ';' -- ;\n, \"a;b\" /* ; */ ;"), Scan::Ends(33)); + assert_eq!(scan("SELECT 'it''s; here'"), Scan::Open); + assert_eq!(scan("SELECT 1\n"), Scan::Open); + assert_eq!(scan("/* not closed ;"), Scan::Open); + assert_eq!(scan(" -- done\n/* and */\n"), Scan::Blank); + assert_eq!(scan(""), Scan::Blank); +} + +#[test] +fn a_table_is_as_wide_as_its_widest_cell() { + let columns = ["id".to_owned(), "body".to_owned()]; + let rows = [ + vec![Value::Int64(1), Value::Text("première".to_owned())], + vec![Value::Int64(20), Value::Null], + ]; + assert_eq!( + table(&columns, &rows), + "\ ++----+----------+ +| id | body | ++----+----------+ +| 1 | première | +| 20 | NULL | ++----+----------+ +(2 rows) +" + ); + assert_eq!(table(&columns, &[]), "(0 rows)\n"); +} diff --git a/v2/contextdb-cli/tests/shell.rs b/v2/contextdb-cli/tests/shell.rs new file mode 100644 index 000000000..6e9b49d14 --- /dev/null +++ b/v2/contextdb-cli/tests/shell.rs @@ -0,0 +1,732 @@ +//! Runs the built `contextdb` program the way a script does: statements piped to its standard +//! input, and its output, its error stream and its exit status read back. +//! +//! Each test has its own folder under the build's temporary folder, left behind for inspection. + +use std::io::{ErrorKind, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +use contextdb::{Database, params}; +use serde_json::{Value as Json, json}; + +/// What one run of the program gave back. +struct Run { + stdout: String, + stderr: String, + code: Option, +} + +impl Run { + /// The lines of the error stream that report a refused statement to a person. + fn errors(&self) -> Vec<&str> { + self.stderr + .lines() + .filter(|line| line.starts_with("Error:")) + .collect() + } +} + +/// An empty folder of this test's own. +fn folder(test: &str) -> PathBuf { + let folder = Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!("shell-{test}")); + if folder.exists() { + std::fs::remove_dir_all(&folder).unwrap(); + } + std::fs::create_dir_all(&folder).unwrap(); + folder +} + +/// Runs `contextdb` in `dir` with these arguments and this text piped to its standard input. +/// +/// The input is written before anything is read back. Every input here is far smaller than a +/// pipe holds, so the write cannot wait on the program. +fn shell(dir: &Path, args: &[&str], input: &str) -> Run { + let mut child = Command::new(env!("CARGO_BIN_EXE_contextdb")) + .args(args) + .current_dir(dir) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + // A program that refuses its command line exits without reading its input. + match child.stdin.take().unwrap().write_all(input.as_bytes()) { + Err(error) if error.kind() != ErrorKind::BrokenPipe => panic!("{error}"), + _ => {} + } + let output = child.wait_with_output().unwrap(); + let run = Run { + stdout: String::from_utf8(output.stdout).unwrap(), + stderr: String::from_utf8(output.stderr).unwrap(), + code: output.status.code(), + }; + assert!( + !run.stderr.contains("panicked at"), + "the program panicked:\n{}", + run.stderr + ); + run +} + +/// A path as a command-line argument. +fn arg(path: &Path) -> &str { + path.to_str().unwrap() +} + +/// Every line of a stream as a JSON document. +fn documents(stream: &str) -> Vec { + stream + .lines() + .map(|line| { + serde_json::from_str(line).unwrap_or_else(|_| panic!("not a JSON document: {line}")) + }) + .collect() +} + +/// The names in a folder, sorted. +fn listing(folder: &Path) -> Vec { + let mut names: Vec = std::fs::read_dir(folder) + .unwrap() + .map(|entry| entry.unwrap().file_name().to_string_lossy().into_owned()) + .collect(); + names.sort(); + names +} + +const CREATE_NOTES: &str = "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT);\n"; +const SELECT_NOTES: &str = "SELECT id, body FROM notes ORDER BY id;\n"; + +const ONE_NOTE: &str = "\ ++----+-------+ +| id | body | ++----+-------+ +| 1 | first | ++----+-------+ +(1 row) +"; + +const TWO_NOTES: &str = "\ ++----+--------+ +| id | body | ++----+--------+ +| 1 | first | +| 2 | second | ++----+--------+ +(2 rows) +"; + +#[test] +fn reading_a_missing_store_is_refused_and_creates_nothing() { + let dir = folder("reading-a-missing-store"); + let store = dir.join("store"); + + let run = shell(&dir, &[arg(&store)], SELECT_NOTES); + assert_eq!(run.code, Some(1), "{}", run.stderr); + assert_eq!(run.stdout, ""); + assert!( + run.stderr.contains("there is no store at this path") + && run.stderr.contains("--write creates it"), + "{}", + run.stderr + ); + assert_eq!( + listing(&dir), + Vec::::new(), + "a refused read creates nothing" + ); + + // The same refusal for a program that reads it: one document on the error stream. + let run = shell(&dir, &[arg(&store), "--json"], SELECT_NOTES); + assert_eq!(run.code, Some(1), "{}", run.stderr); + assert_eq!(run.stdout, ""); + let refusals = documents(&run.stderr); + assert_eq!(refusals.len(), 1, "{}", run.stderr); + assert_eq!(refusals[0]["error"]["detail"]["kind"], "store_not_found"); + assert_eq!( + listing(&dir), + Vec::::new(), + "a refused read creates nothing" + ); +} + +#[test] +fn write_creates_a_missing_store() { + let dir = folder("write-creates-a-missing-store"); + let store = dir.join("store"); + + let insert = "INSERT INTO notes (id, body) VALUES (1, 'first');\n"; + let run = shell( + &dir, + &[arg(&store), "--write"], + &format!("{CREATE_NOTES}{insert}"), + ); + assert_eq!(run.code, Some(0), "{}", run.stderr); + assert_eq!(run.errors(), Vec::<&str>::new()); + assert!( + store.is_dir(), + "the store is one folder at the path that was given" + ); + assert_eq!( + listing(&dir), + ["store"], + "nothing is written beside the store's folder" + ); + + // It was a whole read-write session: the next shell reads what it wrote. + let run = shell(&dir, &[arg(&store)], SELECT_NOTES); + assert_eq!(run.code, Some(0), "{}", run.stderr); + assert_eq!(run.stdout, ONE_NOTE); + + // On a store that is there, --write opens it and keeps what it holds. + let insert = "INSERT INTO notes (id, body) VALUES (2, 'second');\n"; + let run = shell( + &dir, + &[arg(&store), "--write"], + &format!("{insert}{SELECT_NOTES}"), + ); + assert_eq!(run.code, Some(0), "{}", run.stderr); + assert!(run.stdout.contains(TWO_NOTES), "{}", run.stdout); +} + +#[test] +fn a_removed_option_is_refused_by_name_with_exit_2_before_the_store_opens() { + let dir = folder("a-removed-option-is-refused"); + let store = dir.join("store"); + let removed = [ + ["--read-owner-connect-ms", "1000"], + ["--read-owner-routing-retry-ms", "1000"], + ["--read-owner-response-ms", "11000"], + ["--owner-read-concurrency", "4"], + ["--owner-read-request-ms", "10000"], + ["--owner-read-result-rows", "500"], + ]; + for [option, value] in removed { + // With --write a store would be created here if the program got as far as opening one. + for extra in [&[][..], &["--json"][..]] { + let mut args = vec![arg(&store), "--write", option, value]; + args.extend_from_slice(extra); + let run = shell(&dir, &args, CREATE_NOTES); + assert_eq!(run.code, Some(2), "{option}: {}", run.stderr); + assert!( + run.stderr.contains(option), + "{option} is not named in: {}", + run.stderr + ); + assert_eq!(run.stdout, "", "{option}"); + // The command line is refused in the argument parser's own words, with or + // without --json. + assert!( + !run.stderr.trim_start().starts_with('{'), + "{option}: {}", + run.stderr + ); + assert_eq!( + listing(&dir), + Vec::::new(), + "{option}: the store was opened" + ); + } + } + + // An option the program never had, and a command line with no store on it, are refused the + // same way: exit 2 in the argument parser's words, with nothing opened or created. + let unknown = shell( + &dir, + &[arg(&store), "--write", "--frobnicate"], + CREATE_NOTES, + ); + assert_eq!(unknown.code, Some(2), "{}", unknown.stderr); + assert!( + unknown.stderr.contains("--frobnicate"), + "{}", + unknown.stderr + ); + assert_eq!(unknown.stdout, ""); + for no_path in [&[][..], &["--write"][..], &["--write", "--json"][..]] { + let run = shell(&dir, no_path, CREATE_NOTES); + assert_eq!(run.code, Some(2), "{no_path:?}: {}", run.stderr); + assert_eq!(run.stdout, "", "{no_path:?}"); + assert!( + !run.stderr.trim_start().starts_with('{'), + "{no_path:?}: {}", + run.stderr + ); + } + assert_eq!( + listing(&dir), + Vec::::new(), + "a refused command line created something" + ); +} + +#[test] +fn a_folder_named_like_a_command_opens_when_written_as_a_path() { + let dir = folder("a-folder-named-like-a-command"); + let made = shell( + &dir, + &["./server", "--write"], + "CREATE TABLE notes (id INTEGER PRIMARY KEY);\nINSERT INTO notes (id) VALUES (7);\n", + ); + assert_eq!(made.code, Some(0), "{}", made.stderr); + assert!(dir.join("server").is_dir()); + + // Written as a path, the folder is the store. + let as_path = shell(&dir, &["./server"], "SELECT id FROM notes;\n"); + assert_eq!(as_path.code, Some(0), "{}", as_path.stderr); + assert_eq!( + as_path.stdout, + "+----+\n| id |\n+----+\n| 7 |\n+----+\n(1 row)\n" + ); + + // Written bare, the word is the command, which this version refuses by name; the store + // beside it is not opened as a shell. + let as_command = shell(&dir, &["server"], "SELECT id FROM notes;\n"); + assert_ne!(as_command.code, Some(0)); + assert!( + as_command.stderr.contains("server"), + "{}", + as_command.stderr + ); + assert!(!as_command.stdout.contains("| 7"), "{}", as_command.stdout); +} + +#[test] +fn piped_statements_all_run_and_a_refused_one_fails_the_run() { + let dir = folder("piped-statements-all-run"); + let store = dir.join("store"); + let input = "\ +CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT); +.tables +INSERT INTO notes (id, body) VALUES (1, 'first'); +INSERT INTO notes (id, body) VALUES (1, 'again'); +SELECT id, + body + FROM notes + ORDER BY id; +SELECT nope FROM notes; +INSERT INTO notes (id, body) VALUES (2, 'second'); +SELECT COUNT(*) AS n FROM notes; +"; + let run = shell(&dir, &[arg(&store), "--write"], input); + + // A statement may span lines, a command is one line, and every result is on the output. + let first = run + .stdout + .find(ONE_NOTE) + .expect("the statement written over four lines ran"); + assert!( + run.stdout.lines().any(|line| line == "notes"), + "{}", + run.stdout + ); + + // Each refused statement is reported on the error stream, never as a result and never as + // a success: three statements changed something and three say so. + assert_eq!(run.errors().len(), 2, "{}", run.stderr); + assert!(!run.stdout.contains("Error"), "{}", run.stdout); + let ok: Vec<&str> = run + .stdout + .lines() + .filter(|line| line.starts_with("ok (")) + .collect(); + assert_eq!( + ok, + [ + "ok (rows_affected=0)", + "ok (rows_affected=1)", + "ok (rows_affected=1)" + ] + ); + + // The statements after a refused one still ran. + let count = "+---+\n| n |\n+---+\n| 2 |\n+---+\n(1 row)\n"; + let last = run + .stdout + .find(count) + .expect("the statements after a refused one ran"); + assert!(first < last); + + // No prompt and no banner on either stream, and the run failed because a statement did. + for stream in [&run.stdout, &run.stderr] { + assert!( + !stream.contains("contextdb>") && !stream.contains("contextdb(ro)>"), + "{stream}" + ); + assert!(!stream.contains("session"), "{stream}"); + } + assert_eq!(run.code, Some(1)); + + // A run in which every statement succeeded exits 0 and prints its results and nothing else. + let run = shell(&dir, &[arg(&store)], SELECT_NOTES); + assert_eq!(run.code, Some(0), "{}", run.stderr); + assert_eq!(run.stdout, TWO_NOTES); +} + +#[test] +fn json_prints_one_document_per_statement_with_columns_in_declared_order() { + let dir = folder("json-prints-one-document-per-statement"); + let store = dir.join("store"); + let input = "\ +CREATE TABLE notes (id INTEGER PRIMARY KEY, zeta TEXT, alpha TEXT); +INSERT INTO notes (id, zeta, alpha) VALUES (1, 'z1', 'a1'), (2, 'z2', NULL); +SELECT zeta, id, alpha FROM notes ORDER BY id; +SELECT * FROM notes WHERE id = 404; +SELECT nope FROM notes; +.tables +"; + let run = shell(&dir, &[arg(&store), "--write", "--json"], input); + + // One document per statement that gave a result, in the order they were given. `columns` + // is the order the statement or the table declared, which the keys of a row cannot carry. + let results = documents(&run.stdout); + assert_eq!(results.len(), 5, "{}", run.stdout); + assert_eq!(results[0], json!({"rows_affected": 0})); + assert_eq!(results[1], json!({"rows_affected": 2})); + assert_eq!( + results[2], + json!({"result": { + "columns": ["zeta", "id", "alpha"], + "rows": [ + {"zeta": "z1", "id": 1, "alpha": "a1"}, + {"zeta": "z2", "id": 2, "alpha": null}, + ], + }}) + ); + assert_eq!( + results[3], + json!({"result": {"columns": ["id", "zeta", "alpha"], "rows": []}}) + ); + assert_eq!(results[4]["tables"]["items"], json!(["notes"])); + + // The keys of a row come in the order of `columns` too, so a consumer that reads the row + // as written sees zeta, id, alpha and not the alphabet. + let line = run.stdout.lines().nth(2).unwrap(); + let rows = &line[line.find("\"rows\"").expect("the document has rows")..]; + let at = |key: &str| { + rows.find(&format!("\"{key}\"")) + .expect("the row has the key") + }; + assert!(at("zeta") < at("id") && at("id") < at("alpha"), "{rows}"); + + // The refused statement is a document too, on the error stream. + let refusals: Vec = documents(&run.stderr) + .into_iter() + .filter(|document| document.get("error").is_some()) + .collect(); + assert_eq!(refusals.len(), 1, "{}", run.stderr); + assert_eq!(refusals[0]["error"]["class"], "sql"); + assert!(refusals[0]["error"]["message"].is_string()); + assert_eq!(run.code, Some(1)); +} + +#[test] +fn a_read_shell_refuses_a_write_by_name() { + let dir = folder("a-read-shell-refuses-a-write"); + let store = dir.join("store"); + let insert = "INSERT INTO notes (id, body) VALUES (1, 'first');\n"; + let made = shell( + &dir, + &[arg(&store), "--write"], + &format!("{CREATE_NOTES}{insert}"), + ); + assert_eq!(made.code, Some(0), "{}", made.stderr); + + let writes = "\ +INSERT INTO notes (id, body) VALUES (2, 'second'); +UPDATE notes SET body = 'changed'; +DELETE FROM notes; +CREATE TABLE other (id INTEGER PRIMARY KEY); +BEGIN; +"; + // Each of the five is refused, telling the person what to add, and the read after them + // answers with the row as it was. + let run = shell(&dir, &[arg(&store)], &format!("{writes}{SELECT_NOTES}")); + assert_eq!(run.code, Some(1)); + assert_eq!(run.errors().len(), 5, "{}", run.stderr); + assert!( + run.errors().iter().all(|line| line.contains("--write")), + "{}", + run.stderr + ); + assert!(run.stdout.contains(ONE_NOTE), "{}", run.stdout); + assert!(!run.stdout.contains("ok ("), "{}", run.stdout); + + // A program reads the refusal's kind. + let run = shell(&dir, &[arg(&store), "--json"], writes); + assert_eq!(run.code, Some(1)); + let refusals: Vec = documents(&run.stderr) + .into_iter() + .filter(|document| document.get("error").is_some()) + .collect(); + assert_eq!(refusals.len(), 5, "{}", run.stderr); + for refusal in &refusals { + assert_eq!( + refusal["error"]["detail"]["kind"], "write_requires_flag", + "{refusal}" + ); + } + + // Nothing was written: one row, one table. + let run = shell( + &dir, + &[arg(&store), "--json"], + &format!("{SELECT_NOTES}.tables\n"), + ); + assert_eq!(run.code, Some(0), "{}", run.stderr); + let results = documents(&run.stdout); + assert_eq!( + results[0]["result"]["rows"], + json!([{"id": 1, "body": "first"}]) + ); + assert_eq!(results[1]["tables"]["items"], json!(["notes"])); +} + +#[test] +fn a_read_shell_answers_while_a_writer_holds_the_store() { + let dir = folder("a-read-shell-answers-while-a-writer-holds"); + let store = dir.join("store"); + + // This test is the writer, and holds the store until the end. + let writer = Database::open(&store).unwrap(); + writer + .execute( + "CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT)", + ¶ms! {}, + ) + .unwrap(); + writer + .execute( + "INSERT INTO notes (id, body) VALUES (1, 'first')", + ¶ms! {}, + ) + .unwrap(); + + // The store really is held: a second writer is refused, naming this process. + let second = shell(&dir, &[arg(&store), "--write"], SELECT_NOTES); + assert_eq!(second.code, Some(1), "{}", second.stderr); + assert!( + second.stderr.contains(&std::process::id().to_string()), + "{}", + second.stderr + ); + + // A read shell needs nothing from the writer and answers. + let run = shell(&dir, &[arg(&store)], SELECT_NOTES); + assert_eq!(run.code, Some(0), "{}", run.stderr); + assert_eq!(run.stdout, ONE_NOTE); + + // The writer was not held up by it, and the next read shell reads its next commit. + writer + .execute( + "INSERT INTO notes (id, body) VALUES (2, 'second')", + ¶ms! {}, + ) + .unwrap(); + let run = shell(&dir, &[arg(&store)], SELECT_NOTES); + assert_eq!(run.code, Some(0), "{}", run.stderr); + assert_eq!(run.stdout, TWO_NOTES); + + writer.close().unwrap(); +} + +#[test] +fn begin_commit_and_rollback_run_a_transaction() { + let dir = folder("begin-commit-and-rollback"); + let store = dir.join("store"); + let input = "\ +CREATE TABLE notes (id INTEGER PRIMARY KEY, body TEXT); +BEGIN; +INSERT INTO notes (id, body) VALUES (1, 'first'); +INSERT INTO notes (id, body) VALUES (2, 'second'); +COMMIT; +BEGIN; +INSERT INTO notes (id, body) VALUES (3, 'third'); +DELETE FROM notes WHERE id = 1; +SELECT id FROM notes ORDER BY id; +ROLLBACK; +SELECT id, body FROM notes ORDER BY id; +"; + let run = shell(&dir, &[arg(&store), "--write"], input); + assert_eq!(run.code, Some(0), "{}", run.stderr); + assert_eq!(run.errors(), Vec::<&str>::new()); + + // Inside the second transaction its own changes are read; after ROLLBACK they are gone and + // what the first one committed is there. + let inside = "+----+\n| id |\n+----+\n| 2 |\n| 3 |\n+----+\n(2 rows)\n"; + let seen_inside = run + .stdout + .find(inside) + .expect("a transaction reads its own changes"); + let seen_after = run + .stdout + .find(TWO_NOTES) + .expect("ROLLBACK left what COMMIT stored"); + assert!(seen_inside < seen_after); + + // The committed transaction reached the store, and the rolled-back one never did. + let run = shell(&dir, &[arg(&store)], SELECT_NOTES); + assert_eq!(run.code, Some(0), "{}", run.stderr); + assert_eq!(run.stdout, TWO_NOTES); +} + +#[test] +fn tables_and_schema_print_what_was_declared() { + let dir = folder("tables-and-schema"); + let store = dir.join("store"); + // What `.schema notes` prints: the declaration as SQL. A column with no rule prints its name + // and type alone, and nothing the author did not write is printed: no index behind the key + // or the unique rules, no table option. + let notes = "\ +CREATE TABLE notes ( + id INTEGER PRIMARY KEY, + title TEXT NOT NULL, + body TEXT, + stars INTEGER DEFAULT 3, + slug TEXT UNIQUE, + UNIQUE (title, stars) +); +"; + let authors = "CREATE TABLE authors (id UUID PRIMARY KEY, name TEXT);\n"; + let made = shell( + &dir, + &[arg(&store), "--write"], + &format!("{notes}{authors}"), + ); + assert_eq!(made.code, Some(0), "{}", made.stderr); + + // `.tables` lists the names in order, one a line. + let tables = shell(&dir, &[arg(&store)], ".tables\n"); + assert_eq!(tables.code, Some(0), "{}", tables.stderr); + let names: Vec<&str> = tables + .stdout + .lines() + .filter(|line| ["authors", "notes"].contains(line)) + .collect(); + assert_eq!(names, ["authors", "notes"], "{}", tables.stdout); + + let schema = shell(&dir, &[arg(&store)], ".schema notes\n"); + assert_eq!(schema.code, Some(0), "{}", schema.stderr); + assert_eq!(schema.stdout, notes); + + // Each command has a short spelling that prints the same. + assert_eq!(shell(&dir, &[arg(&store)], "\\dt\n").stdout, tables.stdout); + assert_eq!( + shell(&dir, &[arg(&store)], "\\d notes\n").stdout, + schema.stdout + ); + + // What `.schema` printed declares the same table again in another store. + let copy = dir.join("copy"); + let made = shell(&dir, &[arg(©), "--write"], &schema.stdout); + assert_eq!(made.code, Some(0), "{}", made.stderr); + assert_eq!(shell(&dir, &[arg(©)], ".schema notes\n").stdout, notes); +} + +#[test] +fn the_memory_limit_option_is_the_sessions_ceiling() { + let dir = folder("the-memory-limit-option"); + let store = dir.join("store"); + + // The option is the ceiling the session reports, in bytes, with K, M and G as powers of + // 1024; without it there is none. + let limit = |args: &[&str]| { + let run = shell(&dir, args, "SHOW MEMORY_LIMIT;\n"); + assert_eq!(run.code, Some(0), "{}", run.stderr); + documents(&run.stdout)[0]["result"]["rows"][0]["limit"].clone() + }; + assert_eq!(limit(&[arg(&store), "--write", "--json"]), json!("none")); + for (size, bytes) in [ + ("65536K", 67_108_864_u64), + ("64M", 67_108_864), + ("1G", 1_073_741_824), + ] { + let args = [arg(&store), "--write", "--json", "--memory-limit", size]; + assert_eq!(limit(&args), json!(bytes), "--memory-limit {size}"); + } + + // A read shell answers the same statement without --write, with all five columns and with + // the ceiling of its own session. + let columns = [ + "limit", + "used", + "available", + "startup_ceiling", + "ceiling_refresh_failures", + ]; + let run = shell(&dir, &[arg(&store), "--json"], "SHOW MEMORY_LIMIT;\n"); + assert_eq!(run.code, Some(0), "{}", run.stderr); + assert_eq!( + documents(&run.stdout)[0]["result"]["columns"], + json!(columns) + ); + let args = [arg(&store), "--json", "--memory-limit", "64M"]; + assert_eq!( + limit(&args), + json!(67_108_864_u64), + "a read shell's --memory-limit" + ); + + // A size that cannot be read is a mistake in the command line: exit 2, nothing opened. + let unread = dir.join("unread"); + let run = shell( + &dir, + &[arg(&unread), "--write", "--memory-limit", "12Q"], + CREATE_NOTES, + ); + assert_eq!(run.code, Some(2), "{}", run.stderr); + assert!(!unread.exists()); + + // 64 rows of one mebibyte each, written with no ceiling. + let big = dir.join("big"); + let writer = Database::open(&big).unwrap(); + writer + .execute( + "CREATE TABLE blobs (id INTEGER PRIMARY KEY, body TEXT)", + ¶ms! {}, + ) + .unwrap(); + for id in 1..=64_i64 { + let body = format!("{id:08}").repeat(131_072); + writer + .execute( + "INSERT INTO blobs (id, body) VALUES ($id, $body)", + ¶ms! { "id" => id, "body" => body }, + ) + .unwrap(); + } + writer.close().unwrap(); + + // Under a 16 MiB ceiling, reading or writing, the statement that would bring all 64 MiB + // into memory is refused by name with the numbers of the refusal, and gives no result; the + // session goes on, and a statement that fits is answered. + let statements = "SELECT id, body FROM blobs ORDER BY id;\nSELECT COUNT(*) AS n FROM blobs;\n"; + for mode in [&[][..], &["--write"][..]] { + let mut args = vec![arg(&big), "--json", "--memory-limit", "16M"]; + args.extend_from_slice(mode); + let run = shell(&dir, &args, statements); + assert_eq!(run.code, Some(1), "{mode:?}: {}", run.stderr); + let refusals: Vec = documents(&run.stderr) + .into_iter() + .filter(|document| document.get("error").is_some()) + .collect(); + assert_eq!(refusals.len(), 1, "{mode:?}: {}", run.stderr); + let error = &refusals[0]["error"]; + assert_eq!(error["class"], "io", "{mode:?}: {error}"); + assert_eq!( + error["detail"]["kind"], "memory_budget_exceeded", + "{mode:?}: {error}" + ); + for number in ["requested_bytes", "available_bytes", "budget_limit_bytes"] { + assert!( + error["detail"][number].is_u64(), + "{mode:?}: no {number} in {error}" + ); + } + assert_eq!( + documents(&run.stdout), + [json!({"result": {"columns": ["n"], "rows": [{"n": 64}]}})], + "{mode:?}" + ); + } +} diff --git a/v2/contextdb/Cargo.toml b/v2/contextdb/Cargo.toml new file mode 100644 index 000000000..1002a1d08 --- /dev/null +++ b/v2/contextdb/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "contextdb" +version = "2.0.0" +edition = "2024" +rust-version = "1.89" +license = "Apache-2.0" +description = "An embedded SQL database in one folder: typed tables, transactions, one writer with any number of readers in other processes, and a memory limit that holds" +repository = "https://github.com/context-graph-ai/contextdb" +readme = "README.md" + +[dependencies] +pest = "2.8.6" +pest_derive = "2.8.6" +redb = { package = "contextdb-redb", version = "4.1.0", path = "../redb" } +# Numbers in a stored JSON value are read back exactly as they were written. +serde_json = { version = "1", features = ["float_roundtrip"] } +uuid = { version = "1", features = ["v4"] } + +[lints] +workspace = true diff --git a/v2/contextdb/README.md b/v2/contextdb/README.md new file mode 100644 index 000000000..0d75e9cbf --- /dev/null +++ b/v2/contextdb/README.md @@ -0,0 +1,251 @@ +# contextdb + +An embedded SQL database: tables in one folder, inside your own process. + +This is the first version of contextdb 2. What it has: + +- tables with typed columns, a primary key, `NOT NULL`, `UNIQUE` and `DEFAULT`, and indexes; +- `INSERT`, `UPDATE`, `DELETE` and `SELECT` over one table; +- transactions; +- one process writing a store while any number of other processes read it; +- a memory limit that holds. + +What it does not have yet: vector search, relationships between rows, joins, grouping, and sync +between machines. A statement that uses one of them is refused and changes nothing. + +It is not on crates.io yet. Clone and use it from that checkout: + +```toml +[dependencies] +contextdb = { path = "/v2/contextdb" } +``` + +```rust,no_run +use contextdb::{params, Database, Uuid, Value}; + +fn main() -> contextdb::Result<()> { + let db = Database::open("./notes")?; // a folder, created if it is missing + db.execute( + "CREATE TABLE notes (id UUID PRIMARY KEY, body TEXT NOT NULL, made TIMESTAMP DEFAULT NOW())", + ¶ms! {}, + )?; + db.execute( + "INSERT INTO notes (id, body) VALUES ($id, $body)", + ¶ms! { "id" => Uuid::new_v4(), "body" => "buy milk" }, + )?; + + let tx = db.transaction()?; // rolls back if dropped + tx.execute("UPDATE notes SET body = 'buy oat milk' WHERE body = 'buy milk'", ¶ms! {})?; + tx.commit()?; + + let found = db.execute("SELECT body, made FROM notes ORDER BY made LIMIT 10", ¶ms! {})?; + for row in found.rows() { + if let Value::Text(body) = &row[0] { + println!("{body}"); + } + } + Ok(()) +} +``` + +It prints `buy oat milk`. This page is also the crate's documentation, and the documentation +tests compile this program as it is written here. Rust 1.89 or newer; Linux and macOS. Licence: +Apache-2.0. It is developed in the open at by the context-graph-ai organisation. + +## Statements + +```sql +CREATE TABLE [IF NOT EXISTS] name ( + column type [PRIMARY KEY] [NOT NULL] [UNIQUE] [DEFAULT value], + ..., + [UNIQUE (column, ...)] +); +CREATE INDEX name ON table (column [ASC | DESC], ...); +INSERT INTO table (column, ...) VALUES (value, ...), (value, ...); +UPDATE table SET column = value, ... [WHERE condition]; +DELETE FROM table [WHERE condition]; +SELECT column, ... | * FROM table [WHERE condition] [ORDER BY column [ASC | DESC], ...] [LIMIT n]; +SET MEMORY_LIMIT 512M; SET MEMORY_LIMIT NONE; SHOW MEMORY_LIMIT; +``` + +- Every table has exactly one `PRIMARY KEY` column. A default is a literal or `NOW()`. +- A condition or a value is built from column names, literals, `$name` values, + `= != <> < <= > >=`, `AND`, `OR`, `NOT`, `IS NULL`, `IS NOT NULL`, `+ - * /`, + parentheses and `NOW()`. A comparison with NULL is not true, as in standard SQL. +- `SELECT` also takes `COUNT(*)`, `COUNT(column)`, `SUM`, `MIN` and `MAX` over the rows that + match, and `AS` to name a column of the result. +- `ORDER BY` puts NULL last ascending and first descending. +- `--` and `/* */` comments are read past. + +Not in this version, and refused: `JOIN`, `GROUP BY`, `DISTINCT`, `IN`, `LIKE`, `BETWEEN`, +`OFFSET`, `UNION`, subqueries, table aliases, `ALTER`, `DROP`, a primary key over several +columns, and similarity search (`<=>`). Most are refused by name; this is the sentence the +refusal prints: + +```text +plan error: LIKE is not supported in this version of the library +``` + +## Column types + +| Type | Holds | In Rust | +|---|---|---| +| `INTEGER`, `INT` | a 64-bit signed whole number | `Value::Int64` | +| `REAL`, `FLOAT` | a 64-bit floating-point number | `Value::Float64` | +| `TEXT` | text | `Value::Text` | +| `BOOLEAN`, `BOOL` | `TRUE` or `FALSE` | `Value::Bool` | +| `UUID` | a UUID, written as quoted text | `Value::Uuid` | +| `TIMESTAMP` | milliseconds since the first moment of 1970, UTC; written as ISO 8601 text (`'2026-01-01T00:00:01Z'`) or taken from `NOW()` | `Value::Timestamp` | +| `JSON` | a JSON value, written as quoted text; the value is kept, not the text's spacing | `Value::Json` | +| `VECTOR(n)` | a vector of exactly `n` numbers, written `[0.1, 0.2, 0.3]`; stored and returned, not searchable in this version | `Value::Vector` | + +A value must be of its column's type: text into an `INTEGER` column is refused, and so is NULL +into a `NOT NULL` column, on `INSERT` and on `UPDATE` alike. A whole number is accepted into a +`REAL` column. + +## Binding values + +`$name` in a statement is the entry `"name"` of the map passed with it. `params!` writes the +map: `params! { "id" => Uuid::new_v4(), "body" => "buy milk" }`, or `params! {}` for none. It +takes `bool`, `i64`, `f64`, `&str`, `String`, `Uuid`, a `serde_json::Value` and a `Vec`; +any other value is written as a `Value`, for example `Value::Timestamp(1_767_225_601_000)` or +`Value::Null`. `contextdb::Uuid` and `contextdb::serde_json` are the types the library uses, so +`Uuid::new_v4()` and JSON values need no dependency of your own. + +## Refusals + +Every call that can fail returns `contextdb::Result` with one `contextdb::Error`. A program +matches on the kind (`Error::UniqueViolation { table, column }`, `Error::ColumnNotNullable`, +`Error::TableNotFound`, `Error::DatabaseLocked`, `Error::MemoryBudgetExceeded`, and the rest +listed on the `Error` type); a person reads the sentence it prints: + +```text +unique constraint violation: notes (id) +column notes.body is NOT NULL +column not found: notes.nothing +parse error: syntax error at line 1, column 1: unexpected 'SELEC' +``` + +A refused statement changes nothing. Inside a transaction it leaves the transaction usable. + +## Transactions + +`db.execute` is a transaction of one statement: when it returns, the change is on disk. +`db.transaction()` begins a longer one. Its statements see what it has written and nobody else +does until `commit` returns; dropping it, or `rollback`, discards it. A transaction either +declares tables and indexes or writes rows, not both. When two open transactions insert the same +key, or change the same row, the first to commit wins and the other's `commit` is refused. +`SET MEMORY_LIMIT` is not part of a transaction: run inside one, it takes effect and the store +remembers it at once, and a rollback does not undo it. + +## One writer, many readers + +One process at a time holds a store for writing. A second `Database::open` on the same folder is +refused at once, naming the holder: + +```text +another session holds this store for writing: process 1983518 holds ./notes; open a read session to read it +``` + +Any number of processes read meanwhile with `ReadSession::open`, which never creates anything. +Each statement of a reader sees the newest commit. A reader never makes the writer fail, and +holds it up only for the instant a statement takes to note which commit it reads. A reader keeps +answering when the writer stops, is killed or starts again. + +## The memory limit + +`db.set_memory_limit(Some(bytes))` or `SET MEMORY_LIMIT 512M` declares how much memory the +engine may use for data in the writing process, and the store remembers it until it is changed +or set to `NONE`. A statement that would pass the limit is refused and committed rows stay as +they were: + +```text +memory limit reached during transaction: 30000210 bytes were needed and 7340032 bytes of the 16777216-byte limit were free. select or write less at once, or raise the limit with SET MEMORY_LIMIT or the memory_limit open option +``` + +With nothing declared there is no ceiling. A reader has a limit of its own, given when it opens, +and none unless one is given. Every open sets 9 MiB of the limit aside, 8 MiB for the page cache +and 1 MiB for small fixed things, so 9 MiB is the smallest limit that opens a store and a +statement needs room above it. + +`SHOW MEMORY_LIMIT` returns one row about the session that asks, a writer's or a reader's: +`limit` (its limit in bytes, or `none`), `used` (bytes in use now, the 9 MiB included), +`available` (what is left under the limit), `startup_ceiling` (the limit the session was opened +under) and `ceiling_refresh_failures` (always 0 in this version). + +## The store on disk + +```text +notes/ + data everything committed + lock names the process that holds the store for writing, or held it last + readers/ one small file per reader, while a statement of its runs +``` + +Nothing is written anywhere else. While no process has the folder open for writing it is a plain +folder: copy it, move it or delete it with ordinary tools. Do not copy it while a writer is +running; this version has no command that copies a store beside a live writer. + +**When a process is killed.** Nothing committed is lost: a commit that had returned is there, +and one that was in flight is whole or absent. A killed writer leaves its `lock` file and a +reader killed inside a statement leaves its file in `readers/`; the next open for writing clears +both, and nobody cleans anything by hand. Readers open and answer at once. The next open for writing after a kill +first reads every page the store uses, which takes time that grows with the store, measured on +the machine named below with the file out of the operating system's cache: + +| Store size | Open for writing after a kill | Open after a clean close | +|---|---|---| +| 257 MiB | 72 ms | 0.9 ms | +| 2 GiB | about 0.5 s | 1.2 ms | +| 8 GiB | about 3.4 s | 1.7 ms | + +## The `contextdb` program + +The `contextdb-cli` package beside this one builds `contextdb`, a SQL shell for a store. +Install it from the checkout, then open a store with it: + +```text +cargo install --path /v2/contextdb-cli --locked +contextdb ./notes --write +``` + +```text +contextdb ./notes read the store, also while another process writes it +contextdb ./notes --write read and write it, creating it if it is missing +``` + +A statement ends at `;`. `BEGIN;`, `COMMIT;` and `ROLLBACK;` run a transaction, which needs +`--write`; `.tables` lists +the tables, `.schema
` prints a table's declaration and `.help` lists the commands. With +statements piped in there is no prompt, and a refusal does not stop what comes after it. A +refused statement or command is one line on the error stream, `Error: ` and then the refusal's +sentence, or the program's own sentence where one of its options is the way on; a line that +starts like a command and is not one is answered with `Unknown command:`. The rest still run. A +piped run exits 1 if any statement or command was refused, an unknown command and `.schema` of +a table that is not there included, and 0 otherwise. However the input arrives, the exit status is 1 when the +store could not be opened or closed, and 2 when the command line itself is refused. `--json` +prints one JSON document per statement or command and each refusal as a JSON document on the +error stream. `contextdb --help` has the +options, the exit statuses and the JSON forms. + +## Numbers + +Measured at one commit by `v2/numbers.sh`, which prints them again on any machine: a release +build on a 16-core AMD Ryzen 9 9955HX with 64 GB of memory and an NVMe drive, running Linux and +nothing else. + +| What | Measured | +|---|---| +| 20,000 rows of 1.6 KB, one commit per row: time to insert | 17.2 s | +| That store on disk | 60.3 MiB | +| Open that store | 0.631 ms | +| `SELECT COUNT(*)` over it | 0.026 ms, 0 rows read | +| A million rows under a 64 MiB limit: time to load | 11.9 s | +| Scan the million rows | 246 ms | +| Most memory the engine had allocated during that load and scan | 13.4 MiB | +| Most memory the process held, as the operating system reports it | 18.0 MiB | +| First build of a program that uses the library | 5 s, 46 packages, a 2.7 MiB program | +| From a fresh clone to a passing `cargo test` | 2.1 min | +| Lines of Rust and grammar in the library and the program | 7,369 | +| Tests | 98 | +| Lines that differ from redb 4.1.0 in the storage library under the engine, which is redb 4.1.0 with the changes listed in `v2/redb/README.md` | 577 | diff --git a/v2/contextdb/examples/measure.rs b/v2/contextdb/examples/measure.rs new file mode 100644 index 000000000..c19a110bb --- /dev/null +++ b/v2/contextdb/examples/measure.rs @@ -0,0 +1,203 @@ +//! Prints the numbers this library is held to, one per line as `name value unit`. +//! +//! `measure readme ` runs the README's first program at 20,000 rows of 1.6 KB, one +//! commit per row, and prints the store's size on disk, how long opening it takes and how long +//! `COUNT(*)` takes. `measure million ` loads a million small rows under a 64 MiB limit +//! in commits of 10,000, scans them, and prints the most memory the process had allocated beside +//! the most the operating system saw it hold. The folder must be missing or empty. +//! +//! This program measures. It never passes or fails, no test reads what it prints, and +//! `numbers.sh` is what runs it. + +use std::alloc::{GlobalAlloc, Layout, System}; +use std::fmt::Display; +use std::path::Path; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::{Duration, Instant}; + +use contextdb::{Database, DatabaseOpenOptions, Result, Uuid, params}; + +/// The system allocator, counting the bytes handed out and not yet given back. +struct Counting; + +static IN_USE: AtomicUsize = AtomicUsize::new(0); +static PEAK: AtomicUsize = AtomicUsize::new(0); + +// SAFETY: every request goes to the system allocator unchanged; this only counts its bytes. +unsafe impl GlobalAlloc for Counting { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + let in_use = IN_USE.fetch_add(layout.size(), Ordering::Relaxed) + layout.size(); + PEAK.fetch_max(in_use, Ordering::Relaxed); + unsafe { System.alloc(layout) } + } + + unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) { + IN_USE.fetch_sub(layout.size(), Ordering::Relaxed); + unsafe { System.dealloc(pointer, layout) } + } +} + +#[global_allocator] +static ALLOCATOR: Counting = Counting; + +/// Runs `work` and says how long it took. Apart from the library's `system` part, this is the +/// only place in the workspace that reads a clock. +#[allow( + clippy::disallowed_methods, + reason = "measuring elapsed time is what this program is for" +)] +fn timed(work: impl FnOnce() -> T) -> (T, Duration) { + let start = Instant::now(); + let result = work(); + (result, start.elapsed()) +} + +fn line(name: &str, value: impl Display, unit: &str) { + println!("{name} {value} {unit}"); +} + +fn milliseconds(duration: Duration) -> String { + format!("{:.3}", duration.as_secs_f64() * 1000.0) +} + +fn mebibytes(bytes: u64) -> String { + format!("{:.1}", bytes as f64 / 1_048_576.0) +} + +/// The bytes of every file under a folder. +fn bytes_on_disk(folder: &Path) -> std::io::Result { + let mut bytes = 0; + for entry in std::fs::read_dir(folder)? { + let entry = entry?; + let meta = entry.metadata()?; + bytes += if meta.is_dir() { + bytes_on_disk(&entry.path())? + } else { + meta.len() + }; + } + Ok(bytes) +} + +/// The most memory the operating system has seen this process hold, where it says so. +fn peak_resident_bytes() -> Option { + let status = std::fs::read_to_string("/proc/self/status").ok()?; + let line = status.lines().find(|line| line.starts_with("VmHWM:"))?; + let kibibytes: u64 = line.split_whitespace().nth(1)?.parse().ok()?; + Some(kibibytes * 1024) +} + +/// The README's first program, at 20,000 rows of 1.6 KB. +fn readme(folder: &Path) -> Result<()> { + const ROWS: u128 = 20_000; + let db = Database::open(folder)?; + db.execute( + "CREATE TABLE notes (id UUID PRIMARY KEY, body TEXT)", + ¶ms! {}, + )?; + let (inserted, insert) = timed(|| -> Result<()> { + for n in 1..=ROWS { + let body = format!("note {n:08} ").repeat(115)[..1600].to_owned(); + db.execute( + "INSERT INTO notes (id, body) VALUES ($id, $body)", + ¶ms! { "id" => Uuid::from_u128(n), "body" => body }, + )?; + } + Ok(()) + }); + inserted?; + db.close()?; + line("readme_rows", ROWS, "rows"); + line("readme_insert", format!("{:.1}", insert.as_secs_f64()), "s"); + let on_disk = bytes_on_disk(folder).expect("the store's folder can be listed"); + line("readme_store_size", mebibytes(on_disk), "MiB"); + + let (db, open) = timed(|| Database::open(folder)); + let db = db?; + line("readme_open", milliseconds(open), "ms"); + let (count, counting) = timed(|| db.execute("SELECT COUNT(*) FROM notes", ¶ms! {})); + let count = count?; + line("readme_count_star", milliseconds(counting), "ms"); + line( + "readme_count_star_rows_examined", + count.rows_examined(), + "rows", + ); + db.close() +} + +/// A million small rows loaded and scanned under a 64 MiB limit. +fn million(folder: &Path) -> Result<()> { + const ROWS: i64 = 1_000_000; + const PER_COMMIT: i64 = 10_000; + let mut options = DatabaseOpenOptions::default(); + options.memory_limit = Some(64 << 20); + let db = Database::open_with_options(folder, options)?; + db.execute( + "CREATE TABLE small (id INTEGER PRIMARY KEY, n INTEGER, word TEXT)", + ¶ms! {}, + )?; + let (loaded, load) = timed(|| -> Result<()> { + for first in (1..=ROWS).step_by(PER_COMMIT as usize) { + let tx = db.transaction()?; + for id in first..first + PER_COMMIT { + tx.execute( + "INSERT INTO small (id, n, word) VALUES ($id, $n, $word)", + ¶ms! { "id" => id, "n" => id % 1000, "word" => format!("w{id:07}") }, + )?; + } + tx.commit()?; + } + Ok(()) + }); + loaded?; + let (sum, scan) = timed(|| db.execute("SELECT SUM(n) FROM small", ¶ms! {})); + let sum = sum?; + line( + "million_rows_load", + format!("{:.1}", load.as_secs_f64()), + "s", + ); + line("million_rows_scan", milliseconds(scan), "ms"); + line( + "million_rows_scan_rows_examined", + sum.rows_examined(), + "rows", + ); + line( + "million_rows_peak_allocated", + mebibytes(PEAK.load(Ordering::Relaxed) as u64), + "MiB", + ); + if let Some(bytes) = peak_resident_bytes() { + line("million_rows_peak_resident", mebibytes(bytes), "MiB"); + } + db.close() +} + +fn main() -> Result<()> { + let arguments: Vec = std::env::args().skip(1).collect(); + let [what, folder] = arguments.as_slice() else { + eprintln!("usage: measure readme|million "); + std::process::exit(2); + }; + let folder = Path::new(folder); + if folder + .read_dir() + .is_ok_and(|mut entries| entries.next().is_some()) + { + eprintln!( + "{} is not empty; give a folder that is missing or empty", + folder.display() + ); + std::process::exit(2); + } + match what.as_str() { + "readme" => readme(folder), + "million" => million(folder), + _ => { + eprintln!("usage: measure readme|million "); + std::process::exit(2); + } + } +} diff --git a/v2/contextdb/src/database/mod.rs b/v2/contextdb/src/database/mod.rs new file mode 100644 index 000000000..f9d1e0e13 --- /dev/null +++ b/v2/contextdb/src/database/mod.rs @@ -0,0 +1,551 @@ +//! database: the handle a store is opened for writing with, its transactions, and the one +//! commit function. +//! +//! It never has a second way to commit: a statement run on its own, a transaction and a change +//! of the memory limit each build a change set and hand it to the same function. + +#[cfg(test)] +mod tests; + +use std::cell::RefCell; +use std::collections::HashMap; +use std::path::Path; +use std::sync::{Arc, Mutex, MutexGuard, PoisonError}; + +use crate::memory::Budget; +use crate::query::{self, QueryResult}; +use crate::schema::{self, Catalog}; +use crate::sql::{self, Effect, Plan}; +use crate::store::{ChangeSet, Read, Store, View, WrapStorage}; +use crate::system::{self, Clock}; +use crate::types::{Error, Result, TxId, Value}; +use crate::write; + +/// What a host may say when it opens a store for writing. +/// +/// Start from `DatabaseOpenOptions::default()` and assign the fields that matter. The struct may +/// gain fields in a later version, so it cannot be written out field by field outside this +/// crate. +/// +/// ``` +/// let mut options = contextdb::DatabaseOpenOptions::default(); +/// options.memory_limit = Some(64 << 20); // 64 MiB +/// ``` +#[derive(Clone, Default)] +#[non_exhaustive] +pub struct DatabaseOpenOptions { + /// The most memory, in bytes, that this open may hold for its page cache, its transactions + /// and its results. + /// + /// `None`, the default, takes the limit the store remembers from the last + /// `SET MEMORY_LIMIT` or [`Database::set_memory_limit`], and means no limit when none was + /// ever declared. A limit given here holds until the store is closed and leaves what the + /// store remembers as it is. + /// + /// Every open sets 9 MiB aside out of the limit, 8 MiB of it for the store's page cache. A + /// smaller limit is refused with [`crate::Error::MemoryBudgetExceeded`] and opens no store. + /// + /// A [`crate::ReadSession`] never takes the limit the store remembers: `None` in its own + /// options means no limit at all, so a reader that needs a ceiling is given one there. + pub memory_limit: Option, + /// Where the time comes from for `NOW()` and `DEFAULT NOW()`. `None`, the default, is the + /// machine's clock. + pub clock: Option>, + /// A wrapper around the store's data file: the store opens the file, hands it to this, and + /// reads, writes and syncs it only through what this returns. `None`, the default, uses the + /// file as it is. + pub storage: Option, +} + +/// One store folder, held for writing. +/// +/// One handle holds a store for writing at a time. A second [`Database::open`] of the same +/// folder is refused with [`crate::Error::DatabaseLocked`], from another process and from this +/// one alike. Any number of [`crate::ReadSession`]s, in this process or others, read the store +/// meanwhile. +/// +/// A handle can be shared between threads, and its calls block until they are done. +/// +/// Dropping the handle lets go of the store as [`Database::close`] does. Every commit that +/// returned is already on disk, so neither writes anything. +pub struct Database { + /// The open store, `None` once [`Database::close`] has let go of it. + open: Mutex>>, +} + +/// One store held for writing. A call holds it for as long as the call runs, so the folder is +/// let go when the handle has let go and the last running call has returned. +struct Open { + budget: Budget, + clock: Arc, + /// The limit this open started under, as `SHOW MEMORY_LIMIT` reports it. + started_under: Option, + /// The commit lock: held from before a commit looks at the newest moment until it has + /// replaced it. + committing: Mutex<()>, + /// What every statement and every transaction begins from. Held to copy it, to replace it + /// and to read it again after a read that failed, never while a commit goes to the disk. + newest: Mutex, + /// The last field, so that the view above is dropped before the folder is let go. + store: Store, +} + +/// The newest commit. +struct Newest { + commit: TxId, + /// The tables declared as of it. + catalog: Arc, + /// A view of it. `None` when the read that follows a commit failed: the next call that + /// needs a view reads again, so that nothing is answered from before that commit. + view: Option, +} + +/// What one transaction has staged. +struct Staged { + changes: ChangeSet, + /// The tables as the transaction sees them: those of the moment it began at and those it + /// has declared since. + catalog: Arc, + /// The declarations it ran, in order, for a commit that has to run them again. + declared: Vec, +} + +fn planned(sql: &str) -> Result { + sql::plan(&sql::parse(sql)?) +} + +/// Takes a lock. None of this part's locks is held across a step that can leave what the lock +/// guards half changed, so a holder that panicked left it whole. +fn locked(lock: &Mutex) -> MutexGuard<'_, T> { + lock.lock().unwrap_or_else(PoisonError::into_inner) +} + +impl Open { + /// A view of the newest commit and the tables declared as of it. + fn newest(&self) -> Result<(View, Arc)> { + let mut newest = locked(&self.newest); + if let Some(view) = &newest.view { + return Ok((view.clone(), newest.catalog.clone())); + } + // The tables are read from the view itself, so the two are of one commit even when + // another commit lands in this instant. + let view = self.store.view()?; + let catalog = Arc::new(Catalog::load(&view)?); + *newest = Newest { + commit: view.commit(), + catalog: catalog.clone(), + view: Some(view.clone()), + }; + Ok((view, catalog)) + } + + /// An empty transaction over the newest commit. + fn begin(&self) -> Result { + let (view, catalog) = self.newest()?; + Ok(Staged { + changes: ChangeSet::new(view, &self.budget)?, + catalog, + declared: Vec::new(), + }) + } + + /// Runs one planned statement inside a transaction. A refusal may leave part of the + /// statement staged: the caller rewinds the change set or drops it. + fn run( + &self, + plan: &Plan, + params: &HashMap, + staged: &mut Staged, + ) -> Result { + // One time for the whole statement. + let now = system::now_millis(&*self.clock); + match plan.effect() { + Effect::Declares => { + if staged.declared.is_empty() && !staged.changes.is_empty() { + return Err(Error::MixedSchemaAndRowWrite); + } + let catalog = self.declare(plan, &staged.catalog, &mut staged.changes)?; + staged.catalog = Arc::new(catalog); + staged.declared.push(plan.clone()); + Ok(QueryResult::affected(0)) + } + Effect::WritesRows => { + if !staged.declared.is_empty() { + return Err(Error::MixedSchemaAndRowWrite); + } + let (catalog, changes) = (&staged.catalog, &mut staged.changes); + write::apply(plan, params, catalog, changes, &self.budget, now) + .map(QueryResult::affected) + } + Effect::Reads => { + if let Plan::ShowMemoryLimit = plan { + return Ok(query::memory_limit_shown(&self.budget, self.started_under)); + } + let (catalog, changes) = (&staged.catalog, &staged.changes); + query::run(plan, params, catalog, changes, &self.budget, now) + } + // A setting is not part of the transaction: it takes hold at once, in a commit of + // its own, and a rollback does not undo it. + Effect::Sets(limit) => self + .set_memory_limit(limit) + .map(|()| QueryResult::affected(0)), + } + } + + /// Stages one declaration and, for a new index, its entries for the rows already there. + fn declare(&self, plan: &Plan, catalog: &Catalog, changes: &mut ChangeSet) -> Result { + let declared = schema::declare(plan, catalog, changes)?; + if let Plan::CreateIndex(create) = plan + && let Some(table) = declared.table(&create.table) + && let Some(&index) = table.index_ids.last() + { + write::fill_index(table, index, changes, &self.budget)?; + } + Ok(declared) + } + + /// The one commit function: everything that changes the store is written here, one commit + /// at a time. `_held` is the commit lock. + /// + /// A transaction that began before the newest commit was overtaken. One that declared runs + /// its declarations again over the newest moment, because a table's and an index's number + /// are counted on from the catalog and a new index is filled from the rows that are there; + /// a refusal of that second run is the commit's refusal. One that wrote rows is laid over + /// the newest moment and `write` checks its rows against it. + fn commit(&self, _held: &MutexGuard<'_, ()>, staged: Staged) -> Result<()> { + let Staged { + mut changes, + mut catalog, + declared, + } = staged; + let (view, tables) = self.newest()?; + if changes.commit() != view.commit() { + if !declared.is_empty() { + changes = ChangeSet::new(view, &self.budget)?; + catalog = tables; + for plan in &declared { + catalog = Arc::new(self.declare(plan, &catalog, &mut changes)?); + } + } else if !changes.is_empty() { + let before = changes.rebase(view); + // The rows were staged with the index entries of the tables as they were + // declared then; an index declared since would be left without them. + if before.schema_commit()? != changes.base().schema_commit()? { + return Err(Error::SchemaInvalid { + reason: "a table or an index was declared while this transaction was \ + open, so nothing of it was committed; run it again" + .to_owned(), + }); + } + write::check_at_commit(&changes, &catalog, &before)?; + } + } + if changes.is_empty() { + return Ok(()); + } + let commit = self.store.commit(changes)?; + // The commit is on disk whatever the read after it says. When that read fails no view + // is kept and the next call reads again, so none answers from before this commit. + let view = self.store.view().ok(); + *locked(&self.newest) = Newest { + commit, + catalog, + view, + }; + Ok(()) + } + + /// The one path of `SET MEMORY_LIMIT` and [`Database::set_memory_limit`]: the limit takes + /// hold in this open and is committed for the store to remember, or neither happens. + fn set_memory_limit(&self, limit: Option) -> Result<()> { + let held = locked(&self.committing); + let mut staged = self.begin()?; + staged.changes.put_memory_limit(limit)?; + let before = self.budget.limit(); + // Refused here, before anything is written, when the limit is below what is in use. + self.budget.set_limit(limit)?; + let committed = self.commit(&held, staged); + if committed.is_err() { + // The store did not take the limit, so this open goes back to the one it had. That + // is refused only when another thread has, in this instant, taken more than the old + // limit allows; the new limit then stands until the store is closed. + let _ = self.budget.set_limit(before); + } + committed + } +} + +impl Database { + /// Opens the store in this folder for writing. A folder that is missing or empty becomes a + /// new store, so this never returns [`crate::Error::StoreMissing`]. + /// + /// After a clean close this takes a few milliseconds whatever the store's size. After a + /// writer that was killed, the open first reads every page the store has in use. + /// + /// # Errors + /// + /// [`crate::Error::DatabaseLocked`], at once and without waiting, when another handle holds + /// the store for writing, in another process or in this one. [`crate::Error::StoreIo`] when + /// the folder or a file in it cannot be created, read or written, and + /// [`crate::Error::StoreCorrupted`] when stored data fails its check. + /// [`crate::Error::MemoryBudgetExceeded`] when the memory limit is too small to open a + /// store under. + pub fn open(path: impl AsRef) -> Result { + Database::open_with_options(path, DatabaseOpenOptions::default()) + } + + /// [`Database::open`] with options. + pub fn open_with_options( + path: impl AsRef, + options: DatabaseOpenOptions, + ) -> Result { + let budget = Budget::new(options.memory_limit.map(|limit| limit as u64))?; + let store = Store::open_for_writing(path.as_ref(), &budget, options.storage.as_ref())?; + let view = store.view()?; + // A limit the options name is this open's ceiling. With none named, the ceiling is the + // limit the store remembers. + if options.memory_limit.is_none() { + budget.set_limit(view.memory_limit()?)?; + } + let newest = Newest { + commit: view.commit(), + catalog: Arc::new(Catalog::load(&view)?), + view: Some(view), + }; + let open = Open { + started_under: budget.limit(), + budget, + clock: options.clock.unwrap_or_else(system::machine_clock), + committing: Mutex::new(()), + newest: Mutex::new(newest), + store, + }; + Ok(Database { + open: Mutex::new(Some(Arc::new(open))), + }) + } + + /// The open store, or the refusal of a handle that was closed. + fn opened(&self) -> Result> { + locked(&self.open).clone().ok_or(Error::DatabaseClosed) + } + + /// The open store, for the four calls that return a plain value and so have no way to + /// refuse. On a closed handle they panic with the refusal's sentence. + fn must_be_open(&self) -> Arc { + self.opened().unwrap_or_else(|closed| panic!("{closed}")) + } + + /// Runs one SQL statement in a transaction of its own and returns its result. + /// + /// A statement that changes something is committed before this returns: on `Ok` the change + /// is on disk, on `Err` nothing of it is. + /// + /// Values are bound by name. `$name` in the statement takes the value kept under the key + /// `name`, written without the `$`, in `params`; [`crate::params!`] writes the map. There + /// is no positional form. A `$name` with no key in the map is refused with + /// [`crate::Error::PlanError`], and a key the statement does not use is ignored. + /// + /// This may be called while a [`Transaction`] of the same handle is open, on the same + /// thread too. The statement commits on its own, the open transaction does not see it, and + /// the transaction's commit is then checked against it as [`Transaction::commit`] says. + /// + /// # Errors + /// + /// Whatever refuses the statement, each by its own kind of [`crate::Error`]; + /// [`crate::Error::StoreIo`] or [`crate::Error::StoreCorrupted`] when the store cannot be + /// read or written; [`crate::Error::DatabaseClosed`] on a handle that was closed. + pub fn execute(&self, sql: &str, params: &HashMap) -> Result { + let plan = planned(sql)?; + let open = self.opened()?; + if matches!(plan.effect(), Effect::Declares | Effect::WritesRows) { + // Taken before the statement reads anything, so no other commit can overtake it. + let held = locked(&open.committing); + let mut staged = open.begin()?; + let result = open.run(&plan, params, &mut staged)?; + open.commit(&held, staged)?; + Ok(result) + } else { + // A statement that stages nothing has nothing to commit. + open.run(&plan, params, &mut open.begin()?) + } + } + + /// Begins a transaction over the newest commit. Nothing it does is visible to anyone else + /// until it commits. Several transactions of one handle may be open at once. + /// + /// # Errors + /// + /// [`crate::Error::DatabaseClosed`] on a handle that was closed, and + /// [`crate::Error::StoreIo`] or [`crate::Error::StoreCorrupted`] when the store cannot be + /// read. + pub fn transaction(&self) -> Result> { + Ok(Transaction { + staged: RefCell::new(self.opened()?.begin()?), + db: self, + }) + } + + /// Declares the memory limit in bytes, or no limit for `None`. + /// + /// The limit takes hold at once and is committed, so the store remembers it for every + /// later open whose options name no limit, exactly as `SET MEMORY_LIMIT` does. + /// + /// # Errors + /// + /// [`crate::Error::MemoryBudgetExceeded`] when the limit is below what is in use now, which + /// is never less than the 9 MiB every open sets aside. [`crate::Error::StoreIo`] when the + /// commit cannot be written. In both cases the limit stays what it was. + /// [`crate::Error::DatabaseClosed`] on a handle that was closed. + pub fn set_memory_limit(&self, limit: Option) -> Result<()> { + self.opened()? + .set_memory_limit(limit.map(|limit| limit as u64)) + } + + /// The memory limit in force, in bytes; `None` when there is none. + /// + /// # Panics + /// + /// On a handle that was closed, with the message "database handle is closed". + pub fn memory_limit(&self) -> Option { + let limit = self.must_be_open().budget.limit(); + limit.map(|limit| limit as usize) + } + + /// The bytes counted against the memory limit at this moment: what every open sets aside, + /// and what open transactions and results not yet dropped hold. `SHOW MEMORY_LIMIT` reports + /// the same number as `used`. + /// + /// # Panics + /// + /// On a handle that was closed, with the message "database handle is closed". + pub fn memory_used(&self) -> usize { + self.must_be_open().budget.used() as usize + } + + /// The names of the store's tables as of the newest commit, in name order. A new store has + /// none. + /// + /// # Panics + /// + /// On a handle that was closed, with the message "database handle is closed". + pub fn table_names(&self) -> Vec { + let open = self.must_be_open(); + let newest = locked(&open.newest); + let tables = newest.catalog.tables().into_iter(); + tables.map(|table| table.name.clone()).collect() + } + + /// A table's declaration as SQL text, as of the newest commit: the `CREATE TABLE` statement + /// that would declare the table as it stands, with every clause left at its default not + /// written, then one `CREATE INDEX` statement for each index that was made with one. Each + /// statement ends with `;` and a new line. + /// + /// # Errors + /// + /// [`crate::Error::TableNotFound`] when the store has no table of this name, and + /// [`crate::Error::DatabaseClosed`] on a handle that was closed. The answer is not read + /// from the disk, so no other error is returned. + pub fn declaration(&self, table: &str) -> Result { + let open = self.opened()?; + let newest = locked(&open.newest); + let found = newest.catalog.table(table).map(schema::render); + found.ok_or_else(|| Error::TableNotFound(table.to_owned())) + } + + /// The position of the newest commit on disk: 0 for a store nothing was ever committed to, + /// and one more for every commit since. A transaction that commits nothing does not move + /// it, and no position is ever reported for work that did not commit. + /// + /// The position is for comparing two moments of one store. No call of this library takes + /// one. + /// + /// # Panics + /// + /// On a handle that was closed, with the message "database handle is closed". + pub fn committed_watermark(&self) -> TxId { + locked(&self.must_be_open().newest).commit + } + + /// Lets go of the store, so that the next writer can open it. Every commit that returned is + /// already on disk, and closing writes nothing. While a call is still running on another + /// thread, the store is let go when that call returns. Closing a handle that was closed + /// changes nothing. + /// + /// Every later call on this handle, and on a transaction that was begun from it, returns + /// [`crate::Error::DatabaseClosed`]. Four calls return a plain value and have no way to say + /// so; on a closed handle they panic: [`Database::memory_limit`], + /// [`Database::memory_used`], [`Database::table_names`] and + /// [`Database::committed_watermark`]. + pub fn close(&self) -> Result<()> { + drop(locked(&self.open).take()); + Ok(()) + } +} + +/// A transaction: statements that commit together or not at all. +/// +/// It reads the store as of the commit it began at, together with what it has written itself; +/// nobody else sees its changes until [`Transaction::commit`] returns. Dropping it without +/// committing rolls it back. +/// +/// A transaction can be moved to another thread and cannot be shared between two: it is `Send` +/// and not `Sync`. +pub struct Transaction<'db> { + staged: RefCell, + db: &'db Database, +} + +impl Transaction<'_> { + /// Runs one SQL statement inside the transaction. Values are bound by name, as + /// [`Database::execute`] binds them. + /// + /// A refused statement changes nothing and leaves the transaction usable. A transaction + /// either declares tables and indexes or writes rows: the other kind of statement is + /// refused with [`crate::Error::MixedSchemaAndRowWrite`]. + /// + /// `SET MEMORY_LIMIT` is not part of the transaction. It takes hold at once, and a rollback + /// does not undo it. + pub fn execute(&self, sql: &str, params: &HashMap) -> Result { + let plan = planned(sql)?; + let open = self.db.opened()?; + let staged = &mut *self.staged.borrow_mut(); + let mark = staged.changes.mark(); + let result = open.run(&plan, params, staged); + if result.is_err() { + staged.changes.rewind(mark); + } + result + } + + /// Commits. On `Ok(())` everything the transaction did is on disk. On `Err` none of it is, + /// and the transaction is gone: to try again, begin a new one and run its statements again. + /// + /// # Errors + /// + /// A transaction that was open while another commit landed is checked against that commit: + /// + /// - [`crate::Error::UniqueViolation`] when the other commit took a key, or a value of a + /// unique column, that this transaction inserted; + /// - [`crate::Error::ConditionalUpdateConflict`] when the other commit changed or deleted a + /// row that this transaction changed or deleted; + /// - [`crate::Error::SchemaInvalid`] when a table or an index was declared meanwhile and + /// this transaction wrote rows; + /// - for a transaction that declared tables or indexes, whatever would refuse its + /// statements if they were run now: for one, [`crate::Error::SchemaInvalid`] when a table + /// name it declared has been taken. + /// + /// A statement run with [`Database::execute`] is never overtaken and meets none of these. + /// + /// Also [`crate::Error::StoreIo`] when the commit cannot be written, and + /// [`crate::Error::DatabaseClosed`] when the handle was closed. + pub fn commit(self) -> Result<()> { + let open = self.db.opened()?; + let held = locked(&open.committing); + open.commit(&held, self.staged.into_inner()) + } + + /// Rolls back now. What the transaction staged is held in memory only, so this drops it, + /// writes nothing and cannot fail. Dropping the transaction does the same. + pub fn rollback(self) {} +} diff --git a/v2/contextdb/src/database/tests.rs b/v2/contextdb/src/database/tests.rs new file mode 100644 index 000000000..7913820de --- /dev/null +++ b/v2/contextdb/src/database/tests.rs @@ -0,0 +1,119 @@ +//! What the commit function does for a transaction that another commit overtook, and what a +//! closed handle answers. + +use std::collections::HashMap; + +use super::{Database, Transaction}; +use crate::types::{Error, Value}; + +/// Runs a test on a fresh store in a folder of its own, which is removed when the test passes. +fn with_store(name: &str, test: impl FnOnce(&Database)) { + let folder = std::env::temp_dir().join(format!("contextdb-{name}-{}", std::process::id())); + let db = Database::open(&folder).expect("open a fresh store"); + for sql in [ + "CREATE TABLE t (id INTEGER PRIMARY KEY, tag INTEGER)", + "INSERT INTO t (id, tag) VALUES (1, 7)", + ] { + db.execute(sql, &HashMap::new()).expect(sql); + } + test(&db); + drop(db); + std::fs::remove_dir_all(&folder).expect("remove the test's folder"); +} + +fn rows(db: &Database, sql: &str) -> Vec> { + db.execute(sql, &HashMap::new()).expect(sql).rows().to_vec() +} + +fn ints(numbers: [i64; N]) -> Vec> { + numbers.map(|number| vec![Value::Int64(number)]).to_vec() +} + +#[test] +fn transactions_that_each_declare_a_table_commit_with_a_number_each() { + with_store("declare-side-by-side", |db| { + let declaring = [ + "CREATE TABLE a (id INTEGER PRIMARY KEY)", + "CREATE TABLE b (id INTEGER PRIMARY KEY)", + "CREATE TABLE a (id INTEGER PRIMARY KEY, more TEXT)", + ]; + // All three begin from the same tables. + let [first, second, third] = declaring.map(|sql| { + let tx = db.transaction().expect("begin"); + tx.execute(sql, &HashMap::new()).expect(sql); + tx + }); + first.commit().expect("the first declaration commits"); + second.commit().expect("a table of another name commits"); + // The name was free when the third declared it and is taken now. + assert!(matches!(third.commit(), Err(Error::SchemaInvalid { .. }))); + assert_eq!(db.table_names(), ["a", "b", "t"]); + + // A row of one table is no row of the other. + db.execute("INSERT INTO a (id) VALUES (1)", &HashMap::new()) + .expect("insert into a"); + assert_eq!(rows(db, "SELECT COUNT(*) FROM a"), ints([1])); + assert_eq!(rows(db, "SELECT COUNT(*) FROM b"), ints([0])); + assert!(rows(db, "SELECT id FROM b").is_empty()); + }); +} + +#[test] +fn a_new_index_has_the_rows_committed_while_its_transaction_was_open() { + with_store("index-overtaken", |db| { + let tx = db.transaction().expect("begin"); + tx.execute("CREATE INDEX t_tag ON t (tag)", &HashMap::new()) + .expect("declare the index"); + db.execute("INSERT INTO t (id, tag) VALUES (2, 7)", &HashMap::new()) + .expect("insert while the index is being declared"); + tx.commit().expect("commit the index"); + assert_eq!( + rows(db, "SELECT id FROM t WHERE tag = 7 ORDER BY id"), + ints([1, 2]) + ); + }); +} + +#[test] +fn rows_staged_before_an_index_was_declared_are_not_committed_without_its_entries() { + with_store("rows-overtaken", |db| { + let tx = db.transaction().expect("begin"); + tx.execute("INSERT INTO t (id, tag) VALUES (2, 7)", &HashMap::new()) + .expect("insert in the transaction"); + db.execute("CREATE INDEX t_tag ON t (tag)", &HashMap::new()) + .expect("declare an index beside the transaction"); + assert!(matches!(tx.commit(), Err(Error::SchemaInvalid { .. }))); + assert_eq!(rows(db, "SELECT id FROM t WHERE tag = 7"), ints([1])); + }); +} + +#[test] +fn a_closed_handle_refuses_by_name() { + with_store("closed-handle", |db| { + let tx = db.transaction().expect("begin"); + db.close().expect("close"); + db.close().expect("close a second time"); + let select = "SELECT id FROM t"; + assert!(matches!( + db.execute(select, &HashMap::new()), + Err(Error::DatabaseClosed) + )); + assert!(matches!( + tx.execute(select, &HashMap::new()), + Err(Error::DatabaseClosed) + )); + assert!(matches!(tx.commit(), Err(Error::DatabaseClosed))); + assert!(matches!(db.transaction(), Err(Error::DatabaseClosed))); + }); +} + +fn shared() {} +fn sent() {} + +/// No test may start a thread, so what "calls may be made from many threads" rests on is +/// checked here by the compiler. +#[test] +fn a_handle_is_shared_between_threads_and_a_transaction_moves_to_one() { + shared::(); + sent::>(); +} diff --git a/v2/contextdb/src/lib.rs b/v2/contextdb/src/lib.rs new file mode 100644 index 000000000..77df6f5ac --- /dev/null +++ b/v2/contextdb/src/lib.rs @@ -0,0 +1,37 @@ +#![doc = include_str!("../README.md")] + +mod database; +mod memory; +mod query; +mod read; +mod schema; +mod sql; +mod store; +mod system; +mod types; +mod write; + +pub use database::{Database, DatabaseOpenOptions, Transaction}; +pub use query::QueryResult; +pub use read::{ReadSession, ReadSessionOptions}; +pub use store::{StorageBackend, WrapStorage}; +pub use system::Clock; +pub use types::{ColumnType, Error, Result, TxId, Uuid, Value}; + +/// The JSON package whose `Value` a [`Value::Json`] holds, so a host needs no copy of its own. +pub use serde_json; + +/// Writes the map of named values that [`Database::execute`] binds into a statement. +/// +/// `params! {}` is the empty map. Each value is anything a [`Value`] can be made from. +#[macro_export] +macro_rules! params { + () => { + ::std::collections::HashMap::<::std::string::String, $crate::Value>::new() + }; + ($($name:expr => $value:expr),+ $(,)?) => {{ + let mut map = ::std::collections::HashMap::<::std::string::String, $crate::Value>::new(); + $(map.insert(::std::string::String::from($name), $crate::Value::from($value));)+ + map + }}; +} diff --git a/v2/contextdb/src/memory/mod.rs b/v2/contextdb/src/memory/mod.rs new file mode 100644 index 000000000..719d1c545 --- /dev/null +++ b/v2/contextdb/src/memory/mod.rs @@ -0,0 +1,206 @@ +//! memory: the budget and the reservations taken from it. +//! +//! It never allocates on another part's behalf: it counts what the other parts say they are +//! about to take, and says no before they take it. + +use std::sync::atomic::{AtomicU64, Ordering::SeqCst}; +use std::sync::{Arc, Mutex, PoisonError}; + +use crate::types::{Error, Result}; + +#[cfg(test)] +mod tests; + +/// Bytes every budget sets aside for the small fixed things nobody reserves one by one: a +/// parsed statement and its plan, the table descriptions, a row on its way through. What keeps +/// the figure honest is the test that counts every byte the engine really takes +/// (`tests/memory_ceiling.rs`). +const ALLOWANCE: u64 = 1 << 20; + +/// How "no ceiling" is kept: a limit that no count of bytes can pass. +const NO_CEILING: u64 = u64::MAX; + +/// The declared memory limit of one open store, and how much of it is spoken for. +/// +/// Every figure is in bytes. One budget belongs to one writer or one reading session, and +/// cloning gives another handle to the same budget. The default is no limit: a writer has the +/// limit its store remembers, if one was ever declared, and a reader has one only if its open +/// names one. With no limit there is no ceiling, and what is reserved is still counted. +/// +/// Three kinds of memory are counted against it: small fixed things, covered by an allowance +/// of 1 MiB set aside in [`Budget::new`]; the page cache of 8 MiB, reserved once when the store +/// is opened; and everything that grows with data, which must hold a [`Reservation`]. A limit +/// below those 9 MiB therefore cannot open any store: the open is refused with +/// `MemoryBudgetExceeded`. +#[derive(Clone)] +pub(crate) struct Budget { + counts: Arc, +} + +/// What the handles of one budget share. +/// +/// Every read and write of the two numbers is sequentially consistent, because the limit and +/// the count are judged together: a reservation counts its bytes and then looks at the limit +/// again, a change of the limit publishes it and then looks at the count again, and so one of +/// the two always sees the other. +struct Counts { + /// The limit in bytes, `NO_CEILING` when none is declared. + limit: AtomicU64, + /// Bytes spoken for, to the byte. + used: AtomicU64, + /// Held while the limit changes, so that one change is settled before the next begins. + /// Asking for memory and giving it back never take it. + changing_limit: Mutex<()>, +} + +impl Budget { + /// A budget with this limit in bytes, or with no ceiling for `None`. + /// + /// Refuses with `MemoryBudgetExceeded` a limit too small to hold the fixed allowance. + pub(crate) fn new(limit: Option) -> Result { + let budget = Budget { + counts: Arc::new(Counts { + limit: AtomicU64::new(limit.unwrap_or(NO_CEILING)), + used: AtomicU64::new(0), + changing_limit: Mutex::new(()), + }), + }; + budget.take(ALLOWANCE, "open")?; + Ok(budget) + } + + /// Asks for `bytes` before they are allocated. + /// + /// Refuses with `MemoryBudgetExceeded`, naming `operation`, when the bytes would pass the + /// limit; nothing is counted on refusal. Never waits and never allocates: it costs a few + /// atomic operations, so it is cheap enough to call per row but is meant to be called per + /// batch. + pub(crate) fn reserve(&self, bytes: u64, operation: &'static str) -> Result { + self.take(bytes, operation)?; + Ok(Reservation { + budget: self.clone(), + bytes, + operation, + }) + } + + /// The declared limit in bytes, `None` when there is no ceiling. + pub(crate) fn limit(&self) -> Option { + let limit = self.counts.limit.load(SeqCst); + (limit != NO_CEILING).then_some(limit) + } + + /// Bytes spoken for now: the cache, the allowance and every live reservation. + pub(crate) fn used(&self) -> u64 { + self.counts.used.load(SeqCst) + } + + /// Changes the limit. Refuses a limit below what is already spoken for. + /// + /// Once this returns `Ok`, no more than the new limit is spoken for, for as long as that + /// limit stands. A reservation asked for at the very moment a lower limit is being set may + /// be refused although it would have fit. + pub(crate) fn set_limit(&self, limit: Option) -> Result<()> { + let counts = &*self.counts; + let new = limit.unwrap_or(NO_CEILING); + // The lock guards no data, so a holder that panicked left nothing half done. + let _one_change_at_a_time = counts + .changing_limit + .lock() + .unwrap_or_else(PoisonError::into_inner); + let old = counts.limit.load(SeqCst); + let mut used = counts.used.load(SeqCst); + if used <= new { + counts.limit.store(new, SeqCst); + // Bytes granted against the old limit since the look above are counted by now; + // bytes counted from here on are judged against the new limit. + used = counts.used.load(SeqCst); + if used <= new { + return Ok(()); + } + counts.limit.store(old, SeqCst); + } + Err(refused("set_limit", used, new, new)) + } + + /// Counts `bytes` as spoken for, or refuses and counts nothing. + /// + /// With no ceiling the only request refused is one that would carry the count past what a + /// `u64` holds, which no machine has. + fn take(&self, bytes: u64, operation: &'static str) -> Result<()> { + let counts = &*self.counts; + let mut used = counts.used.load(SeqCst); + loop { + let limit = counts.limit.load(SeqCst); + let available = limit.saturating_sub(used); + if bytes > available { + return Err(refused(operation, bytes, available, limit)); + } + match counts + .used + .compare_exchange_weak(used, used + bytes, SeqCst, SeqCst) + { + Ok(_) if counts.limit.load(SeqCst) == limit => return Ok(()), + // The limit changed between the look and the count: uncount, and ask again. + Ok(_) => used = counts.used.fetch_sub(bytes, SeqCst) - bytes, + Err(now) => used = now, + } + } + } + + fn give_back(&self, bytes: u64) { + self.counts.used.fetch_sub(bytes, SeqCst); + } +} + +/// The refusal of this part. Its labels are fixed words, so making it allocates nothing. +fn refused(operation: &'static str, requested: u64, available: u64, limit: u64) -> Error { + Error::MemoryBudgetExceeded { + subsystem: "memory", + operation, + requested_bytes: requested, + available_bytes: available, + budget_limit_bytes: limit, + hint: "select or write less at once, or raise the limit with SET MEMORY_LIMIT or the \ + memory_limit open option", + } +} + +/// Bytes a caller was granted and has not given back. +/// +/// The holder of memory that grows with data (a result, a sort, a transaction's changes) keeps +/// one of these beside the container and grows it before the container grows. Dropping it +/// gives everything back. +pub(crate) struct Reservation { + budget: Budget, + bytes: u64, + /// What the holder is doing, as a refusal to grow names it. + operation: &'static str, +} + +impl Reservation { + /// Asks for `bytes` more. On refusal the reservation keeps what it had. + pub(crate) fn grow(&mut self, bytes: u64) -> Result<()> { + self.budget.take(bytes, self.operation)?; + self.bytes += bytes; + Ok(()) + } + + /// Gives `bytes` back early, or everything held if that is less. + pub(crate) fn shrink(&mut self, bytes: u64) { + let bytes = bytes.min(self.bytes); + self.budget.give_back(bytes); + self.bytes -= bytes; + } + + /// Bytes held now. + pub(crate) fn bytes(&self) -> u64 { + self.bytes + } +} + +impl Drop for Reservation { + fn drop(&mut self) { + self.budget.give_back(self.bytes); + } +} diff --git a/v2/contextdb/src/memory/tests.rs b/v2/contextdb/src/memory/tests.rs new file mode 100644 index 000000000..03ea151e3 --- /dev/null +++ b/v2/contextdb/src/memory/tests.rs @@ -0,0 +1,85 @@ +use super::{ALLOWANCE, Budget}; +use crate::types::Error; + +/// A budget with `room` bytes left after its allowance, and what it counts as used when new. +fn budget_with_room(room: u64) -> (Budget, u64) { + let limit = ALLOWANCE + room; + let budget = Budget::new(Some(limit)).expect("a limit that holds the allowance"); + assert_eq!(budget.limit(), Some(limit)); + let baseline = budget.used(); + (budget, baseline) +} + +#[test] +fn bytes_are_counted_while_held_and_given_back_when_released() { + let (budget, baseline) = budget_with_room(100); + let mut held = budget.reserve(60, "sort").expect("60 of 100"); + held.grow(30).expect("30 of the 40 left"); + assert_eq!((held.bytes(), budget.used()), (90, baseline + 90)); + held.shrink(50); + assert_eq!((held.bytes(), budget.used()), (40, baseline + 40)); + // Another handle is the same budget. + let other = budget.clone().reserve(60, "result").expect("60 of 60"); + assert_eq!(budget.used(), baseline + 100); + drop(held); + drop(other); + assert_eq!(budget.used(), baseline); +} + +#[test] +fn a_refusal_names_what_was_asked_and_what_was_left_and_counts_nothing() { + let (budget, baseline) = budget_with_room(100); + let mut held = budget.reserve(60, "sort").expect("60 of 100"); + for refusal in [budget.reserve(41, "sort").err(), held.grow(41).err()] { + match refusal { + Some(Error::MemoryBudgetExceeded { + operation: "sort", + requested_bytes: 41, + available_bytes: 40, + budget_limit_bytes, + .. + }) => assert_eq!(budget_limit_bytes, ALLOWANCE + 100), + other => panic!("41 bytes of 40 gave {other:?}"), + } + } + assert_eq!((held.bytes(), budget.used()), (60, baseline + 60)); + held.grow(40).expect("exactly what was left"); +} + +#[test] +fn with_no_limit_every_reservation_succeeds_and_is_counted() { + let budget = Budget::new(None).expect("no ceiling"); + assert_eq!(budget.limit(), None); + let baseline = budget.used(); + let held = budget.reserve(1 << 50, "result").expect("no ceiling"); + assert_eq!(budget.used(), baseline + (1 << 50)); + drop(held); + assert_eq!(budget.used(), baseline); +} + +#[test] +fn a_limit_below_what_is_in_use_is_refused_and_the_old_one_stays() { + assert!(Budget::new(Some(ALLOWANCE - 1)).is_err()); + let (budget, baseline) = budget_with_room(100); + let held = budget.reserve(60, "result").expect("60 of 100"); + match budget.set_limit(Some(baseline + 59)) { + Err(Error::MemoryBudgetExceeded { + requested_bytes, + available_bytes, + .. + }) => assert_eq!( + (requested_bytes, available_bytes), + (baseline + 60, baseline + 59) + ), + other => panic!("a limit one byte below what is in use gave {other:?}"), + } + assert_eq!(budget.limit(), Some(ALLOWANCE + 100)); + budget + .set_limit(Some(baseline + 60)) + .expect("exactly what is in use"); + assert!(budget.reserve(1, "result").is_err()); + budget.set_limit(None).expect("no ceiling"); + assert_eq!(budget.limit(), None); + drop(held); + assert_eq!(budget.used(), baseline); +} diff --git a/v2/contextdb/src/query/access.rs b/v2/contextdb/src/query/access.rs new file mode 100644 index 000000000..bfe6428cd --- /dev/null +++ b/v2/contextdb/src/query/access.rs @@ -0,0 +1,387 @@ +//! Index choice: which rows of a table a statement has to read, and reading them. + +use std::collections::HashMap; +use std::ops::Bound; + +use super::expression::{column_index, eval}; +use super::{heap_bytes, row_overhead}; +use crate::memory::Reservation; +use crate::schema::Table; +use crate::sql::Expr; +use crate::sql::ast::{BinOp, SortDirection as Asked}; +use crate::sql::plan::SortKey; +use crate::store::{Cell, IndexId, Key, Read, Row}; +use crate::types::{ColumnType, Result, SortDirection, Value}; + +/// How the rows of one table are reached. +pub(super) enum Path { + /// The one row with this primary key. + Key(Key), + /// The table between two keys, in key order. + Rows(Bound, Bound), + /// The rows the entries of an index between two entries point at, in entry order. + Index(IndexId, Bound, Bound), +} + +/// A part of a filter that every kept row must pass, and that compares a column with a value +/// known before any row is read. +struct Term { + column: usize, + op: BinOp, + value: Value, +} + +/// Something that keeps the rows of a table in an order: the table itself, under its primary +/// key, or one of its indexes. +struct Ordered { + /// `None` for the table itself. + index: Option, + /// The columns it is ordered by, each with its direction. + columns: Vec<(usize, SortDirection)>, +} + +impl Ordered { + /// The key these leading values make: the bytes every key with these values begins with. + /// A row key is all ascending, which the store is told with no directions. + fn key(&self, values: &[Value]) -> Result { + let directions: Vec = match self.index { + Some(_) => self.columns[..values.len()].iter().map(|c| c.1).collect(), + None => Vec::new(), + }; + Key::from_values(values, &directions) + } +} + +/// The bound that ends a walk after the last key that begins with this one. +fn until_after(key: &Key) -> Bound { + key.prefix_end().map_or(Bound::Unbounded, Bound::Excluded) +} + +/// The bound that starts a walk after the last key that begins with this one. +fn starting_after(key: Key) -> Bound { + let after = key.prefix_end(); + after.map_or(Bound::Excluded(key), Bound::Included) +} + +/// The value as a key of a column of this type holds it, when there is one such value: only +/// then is looking the value up the same as comparing every row with it. +fn as_key(column: ColumnType, value: Value) -> Option { + match (column, value) { + (ColumnType::Integer, value @ Value::Int64(_)) + | (ColumnType::Real, value @ Value::Float64(_)) + | (ColumnType::Text, value @ Value::Text(_)) + | (ColumnType::Boolean, value @ Value::Bool(_)) + | (ColumnType::Uuid, value @ Value::Uuid(_)) + | (ColumnType::Timestamp, value @ Value::Timestamp(_)) => Some(value), + (ColumnType::Real, Value::Int64(value)) => Some(Value::Float64(value as f64)), + (ColumnType::Uuid, Value::Text(value)) => value.parse().ok().map(Value::Uuid), + _ => None, + } +} + +/// One table as one statement reads it. +pub(super) struct Source<'a> { + table: &'a Table, + filter: Option<&'a Expr>, + params: &'a HashMap, + now: i64, + read: &'a dyn Read, + /// The columns the filter reads: taken from every row that is read. + filtered: Vec, + /// The other columns the statement reads: taken only from a row that passed the filter. + rest: Vec, +} + +impl<'a> Source<'a> { + /// `filtered` marks the columns the filter reads and `wanted` every column the statement + /// reads after it. A column in neither is never taken from a row, so a large value in it + /// is never loaded. + pub(super) fn new( + table: &'a Table, + filter: Option<&'a Expr>, + params: &'a HashMap, + now: i64, + read: &'a dyn Read, + filtered: Vec, + wanted: &[bool], + ) -> Source<'a> { + let rest = wanted.iter().zip(&filtered).map(|(w, f)| *w && !*f); + Source { + table, + filter, + params, + now, + read, + rest: rest.collect(), + filtered, + } + } + + /// Picks the path that reads the fewest rows for the filter, and says whether its rows + /// arrive in the order asked for, so that they need no sort. + /// + /// The table and each index are tried in turn. One is better the more of its leading + /// columns the filter gives a value for, then if the filter gives a range of its next + /// column, then if it keeps the order asked for; the table itself wins a tie. The filter is + /// still applied to every row read, so a path only has to reach every row that can pass. + /// + /// Rows are kept under their primary key, so the key is answered from the table itself. Its + /// own index, `__pk_`, is declared and has no entries, and is never walked. + pub(super) fn choose(&self, order: &[SortKey]) -> Result<(Path, bool)> { + let mut terms = Vec::new(); + if let Some(filter) = self.filter { + self.collect(filter, &mut terms)?; + } + let columns = &self.table.meta.columns; + let column_named = |name: &String| columns.iter().position(|column| column.name == *name); + let key = self.key_columns(); + // The order asked for, when it is plain columns. + let asked: Option> = order + .iter() + .map(|key| { + let Expr::Column(column) = &key.expr else { + return None; + }; + let direction = match key.direction { + Asked::Asc => SortDirection::Asc, + Asked::Desc => SortDirection::Desc, + }; + Some((column_index(self.table, column).ok()?, direction)) + }) + .collect(); + + let the_table = Ordered { + index: None, + columns: key.iter().map(|at| (*at, SortDirection::Asc)).collect(), + }; + let indexes = self.table.meta.indexes.iter().zip(&self.table.index_ids); + let indexes = indexes + .filter(|(index, _)| !index.name.starts_with("__pk_")) + .map(|(index, id)| Ordered { + index: Some(*id), + columns: index + .columns + .iter() + .filter_map(|(name, direction)| Some((column_named(name)?, *direction))) + .collect(), + }); + + // Until something better is found: the whole table, in no order that was asked for. + let mut best = ( + (0, false, false), + Path::Rows(Bound::Unbounded, Bound::Unbounded), + ); + for ordered in std::iter::once(the_table).chain(indexes) { + let given: Vec = ordered + .columns + .iter() + .map_while(|(column, _)| { + let equal = |term: &&Term| term.column == *column && term.op == BinOp::Eq; + Some(terms.iter().find(equal)?.value.clone()) + }) + .collect(); + let next = &ordered.columns[given.len()..]; + if ordered.index.is_none() && next.is_empty() { + return Ok((Path::Key(ordered.key(&given)?), true)); + } + let (mut from, mut to) = (Bound::Unbounded, Bound::Unbounded); + if !given.is_empty() { + let all = ordered.key(&given)?; + to = until_after(&all); + from = Bound::Included(all); + } + let mut ranged = false; + if let Some((column, direction)) = next.first() { + let with = |value: &Value| { + let mut values = given.clone(); + values.push(value.clone()); + ordered.key(&values) + }; + let ranges = terms.iter().filter(|term| { + let range = matches!(term.op, BinOp::Gt | BinOp::Gte | BinOp::Lt | BinOp::Lte); + term.column == *column && range + }); + for term in ranges { + if !ranged { + // No NULL is in a range. A key keeps NULL after every value going up + // and before every value going down, and the walk stops short of it. + let null = with(&Value::Null)?; + match direction { + SortDirection::Asc => to = Bound::Excluded(null), + SortDirection::Desc => from = starting_after(null), + } + ranged = true; + } + let key = with(&term.value)?; + // A descending column keeps its greater values first. When more columns + // follow, many keys begin with this one, and "after it" is after them all. + match (term.op, direction) { + (BinOp::Gt, SortDirection::Asc) | (BinOp::Lt, SortDirection::Desc) => { + from = starting_after(key); + } + (BinOp::Gte, SortDirection::Asc) | (BinOp::Lte, SortDirection::Desc) => { + from = Bound::Included(key); + } + (BinOp::Lt, SortDirection::Asc) | (BinOp::Gt, SortDirection::Desc) => { + to = Bound::Excluded(key); + } + // `<=` going up and `>=` going down. + _ => to = until_after(&key), + } + } + } + // A key keeps its columns as ORDER BY sorts them, NULL included, so what follows + // the columns the filter fixed is in order if it is what was asked for. + let in_order = asked.as_ref().is_some_and(|asked| next.starts_with(asked)); + let score = (given.len(), ranged, in_order); + if score > best.0 { + let path = match ordered.index { + Some(index) => Path::Index(index, from, to), + None => Path::Rows(from, to), + }; + best = (score, path); + } + } + let ((_, _, in_order), path) = best; + Ok((path, in_order)) + } + + /// Where the columns of the primary key sit in a row. + pub(super) fn key_columns(&self) -> Vec { + let columns = &self.table.meta.columns; + (0..columns.len()) + .filter(|at| columns[*at].primary_key) + .collect() + } + + /// Gathers the terms of a filter: the comparisons joined by AND at its top. + fn collect(&self, filter: &Expr, terms: &mut Vec) -> Result<()> { + let Expr::BinaryOp { left, op, right } = filter else { + return Ok(()); + }; + if *op == BinOp::And { + self.collect(left, terms)?; + return self.collect(right, terms); + } + let (column, value, op) = match (&**left, &**right) { + (Expr::Column(column), value @ (Expr::Literal(_) | Expr::Parameter(_))) => { + (column, value, *op) + } + // `5 < id` is `id > 5`. + (value @ (Expr::Literal(_) | Expr::Parameter(_)), Expr::Column(column)) => { + let turned = match op { + BinOp::Lt => BinOp::Gt, + BinOp::Lte => BinOp::Gte, + BinOp::Gt => BinOp::Lt, + BinOp::Gte => BinOp::Lte, + other => *other, + }; + (column, value, turned) + } + _ => return Ok(()), + }; + let column = column_index(self.table, column)?; + let value = eval(value, self.table, &[], self.params, self.now)?; + if let Some(value) = as_key(self.table.meta.columns[column].column_type, value) { + terms.push(Term { column, op, value }); + } + Ok(()) + } + + /// Reads the rows the path reaches and hands `each` every one that passes the filter: its + /// key, its values, and the bytes of `reservation` it holds. Stops when `each` answers + /// false. Returns how many stored rows were read. + pub(super) fn walk( + &self, + path: &Path, + reservation: &mut Reservation, + mut each: impl FnMut(Key, Vec, u64, &mut Reservation) -> Result, + ) -> Result { + let mut examined = 0; + let mut offer = |key: Key, row: Row| { + examined += 1; + let before = reservation.bytes(); + match self.values(row, reservation)? { + Some(values) => { + let bytes = reservation.bytes() - before; + each(key, values, bytes, reservation) + } + None => { + reservation.shrink(reservation.bytes() - before); + Ok(true) + } + } + }; + let table = self.table.id; + match path { + Path::Key(key) => { + if let Some(row) = self.read.row(table, key)? { + offer(key.clone(), row)?; + } + } + Path::Rows(from, to) => { + for item in self.read.rows(table, from.as_ref(), to.as_ref())? { + let (key, row) = item?; + if !offer(key, row)? { + break; + } + } + } + Path::Index(index, from, to) => { + for entry in self.read.index(*index, from.as_ref(), to.as_ref())? { + let (_, key) = entry?; + if let Some(row) = self.read.row(table, &key)? + && !offer(key, row)? + { + break; + } + } + } + } + Ok(examined) + } + + /// The values of a stored row if it passes the filter, charged to `reservation`. A column + /// the statement does not read is left as NULL. + fn values(&self, row: Row, reservation: &mut Reservation) -> Result>> { + let mut cells = row.cells; + reservation.grow(row_overhead(cells.len()))?; + let mut values = vec![Value::Null; cells.len()]; + self.take(&mut cells, &mut values, &self.filtered, reservation)?; + if let Some(filter) = self.filter + && eval(filter, self.table, &values, self.params, self.now)? != Value::Bool(true) + { + return Ok(None); + } + self.take(&mut cells, &mut values, &self.rest, reservation)?; + Ok(Some(values)) + } + + /// Moves the marked cells into `values`, loading a value kept beside its row. The memory + /// of the values is asked for before any is moved or loaded. + fn take( + &self, + cells: &mut [Cell], + values: &mut [Value], + marked: &[bool], + reservation: &mut Reservation, + ) -> Result<()> { + let here = cells.iter().zip(marked).map(|(cell, marked)| match cell { + Cell::Here(here) if *marked => heap_bytes(here), + _ => 0, + }); + reservation.grow(here.sum())?; + for ((cell, value), _) in cells + .iter_mut() + .zip(values) + .zip(marked) + .filter(|(_, marked)| **marked) + { + *value = match std::mem::replace(cell, Cell::Here(Value::Null)) { + Cell::Here(here) => here, + Cell::Beside(beside) => self.read.load(&beside, reservation)?, + }; + } + Ok(()) + } +} diff --git a/v2/contextdb/src/query/expression.rs b/v2/contextdb/src/query/expression.rs new file mode 100644 index 000000000..c0a95719a --- /dev/null +++ b/v2/contextdb/src/query/expression.rs @@ -0,0 +1,318 @@ +//! The expression evaluator, and how two values compare and sort. + +use std::cmp::Ordering; +use std::collections::HashMap; + +use super::heap_bytes; +use crate::schema::Table; +use crate::sql::Expr; +use crate::sql::ast::{BinOp, ColumnRef, Literal, UnaryOp}; +use crate::types::{Error, Result, Uuid, Value}; + +/// Works out one expression against one row. The only expression evaluator in the library. +/// +/// `row` holds one value per column of `table`, in declared order, and `now` is the statement's +/// one time in milliseconds, which `NOW()` answers with. A comparison with NULL, and one between +/// values of types that do not compare, is unknown: it gives NULL, and a filter keeps a row +/// only for TRUE. +pub(crate) fn eval( + expr: &Expr, + table: &Table, + row: &[Value], + params: &HashMap, + now: i64, +) -> Result { + let value = |expr: &Expr| eval(expr, table, row, params, now); + Ok(match expr { + Expr::Column(column) => { + let at = column_index(table, column)?; + row.get(at).cloned().ok_or_else(|| missing(table, column))? + } + Expr::Literal(literal) => match literal { + Literal::Null => Value::Null, + Literal::Bool(value) => Value::Bool(*value), + Literal::Integer(value) => Value::Int64(*value), + Literal::Real(value) => Value::Float64(*value), + Literal::Text(value) => Value::Text(value.clone()), + Literal::Vector(value) => Value::Vector(value.clone()), + }, + Expr::Parameter(name) => params.get(name).cloned().ok_or_else(|| unbound(name))?, + Expr::BinaryOp { left, op, right } => { + let holds: fn(Ordering) -> bool = match op { + BinOp::And | BinOp::Or => { + // The value that decides the answer alone: FALSE for AND, TRUE for OR. + let settles = *op == BinOp::Or; + let left = truth(&value(left)?); + if left == Some(settles) { + return Ok(Value::Bool(settles)); + } + return Ok(match (left, truth(&value(right)?)) { + (_, Some(right)) if right == settles => Value::Bool(settles), + (Some(_), Some(_)) => Value::Bool(!settles), + _ => Value::Null, + }); + } + BinOp::Eq => Ordering::is_eq, + BinOp::Neq => Ordering::is_ne, + BinOp::Lt => Ordering::is_lt, + BinOp::Lte => Ordering::is_le, + BinOp::Gt => Ordering::is_gt, + BinOp::Gte => Ordering::is_ge, + }; + compare(&value(left)?, &value(right)?) + .map_or(Value::Null, |order| Value::Bool(holds(order))) + } + Expr::UnaryOp { op, operand } => match (op, value(operand)?) { + (UnaryOp::Not, value) => truth(&value).map_or(Value::Null, |truth| Value::Bool(!truth)), + (UnaryOp::Neg, Value::Null) => Value::Null, + (UnaryOp::Neg, Value::Int64(value)) => { + Value::Int64(value.checked_neg().ok_or_else(out_of_range)?) + } + (UnaryOp::Neg, Value::Float64(value)) => Value::Float64(-value), + (UnaryOp::Neg, _) => { + return Err(Error::PlanError( + "cannot negate a value that is not a number".to_owned(), + )); + } + }, + Expr::IsNull { expr, negated } => Value::Bool((value(expr)? == Value::Null) != *negated), + Expr::FunctionCall { name, args } => match (scalar(name), args.as_slice()) { + (Some(Scalar::Arithmetic(op)), [left, right]) => { + arithmetic(op, &value(left)?, &value(right)?)? + } + (Some(Scalar::Now), []) => Value::Timestamp(now), + (Some(Scalar::Coalesce), args) => { + for arg in args { + let first = value(arg)?; + if first != Value::Null { + return Ok(first); + } + } + Value::Null + } + _ => return Err(refused(name)), + }, + }) +} + +/// The most memory the value of an expression can take outside its own cell. It is known +/// before the expression is worked out, so the memory is asked for before the value is made. +pub(super) fn most_bytes( + expr: &Expr, + table: &Table, + row: &[Value], + params: &HashMap, +) -> u64 { + match expr { + Expr::Column(column) => { + let at = column_index(table, column).ok(); + at.and_then(|at| row.get(at)).map_or(0, heap_bytes) + } + Expr::Literal(Literal::Text(text)) => text.len() as u64, + Expr::Literal(Literal::Vector(vector)) => size_of_val(vector.as_slice()) as u64, + Expr::Parameter(name) => params.get(name).map_or(0, heap_bytes), + Expr::FunctionCall { name, args } if matches!(scalar(name), Some(Scalar::Coalesce)) => { + let each = args.iter().map(|arg| most_bytes(arg, table, row, params)); + each.max().unwrap_or(0) + } + // Everything else gives a number, a truth, a time or NULL. + _ => 0, + } +} + +/// Marks in `read` each column the expression reads. +/// +/// Refuses, before any row is read, what [`eval`] would refuse at the first row: a column the +/// table does not have, a `$name` nothing is bound to, and a form this version does not work +/// out. A statement is therefore refused or not whatever rows its table holds. +pub(super) fn reads( + expr: &Expr, + table: &Table, + params: &HashMap, + read: &mut [bool], +) -> Result<()> { + match expr { + Expr::Column(column) => read[column_index(table, column)?] = true, + Expr::Literal(_) => {} + Expr::Parameter(name) => { + if !params.contains_key(name) { + return Err(unbound(name)); + } + } + Expr::BinaryOp { left, right, .. } => { + reads(left, table, params, read)?; + reads(right, table, params, read)?; + } + Expr::UnaryOp { operand, .. } | Expr::IsNull { expr: operand, .. } => { + reads(operand, table, params, read)?; + } + Expr::FunctionCall { name, args } => { + if scalar(name).is_none() { + return Err(refused(name)); + } + for arg in args { + reads(arg, table, params, read)?; + } + } + } + Ok(()) +} + +/// Where a column sits in a row of its table. A name the table does not have, and a name +/// qualified with anything but the table's own name, is `ColumnNotFound`. +pub(super) fn column_index(table: &Table, column: &ColumnRef) -> Result { + let in_scope = column.table.as_ref().is_none_or(|name| *name == table.name); + table + .meta + .columns + .iter() + .position(|declared| in_scope && declared.name == column.column) + .ok_or_else(|| missing(table, column)) +} + +fn missing(table: &Table, column: &ColumnRef) -> Error { + Error::ColumnNotFound { + table: table.name.clone(), + column: column.column.clone(), + } +} + +fn unbound(name: &str) -> Error { + Error::PlanError(format!("no value is bound to ${name}")) +} + +fn out_of_range() -> Error { + Error::PlanError("integer out of range".to_owned()) +} + +/// The refusal of something this version of the library does not do, by its name. +pub(super) fn unsupported(what: &str) -> Error { + Error::PlanError(format!( + "{what} is not supported in this version of the library" + )) +} + +/// The refusal of a call [`eval`] does not work out. +fn refused(name: &str) -> Error { + unsupported(&format!("the function {name}")) +} + +/// A function that gives one value for one row. +#[derive(Clone, Copy)] +pub(super) enum Scalar { + Arithmetic(Arithmetic), + Now, + /// The first of its values that is not NULL. + Coalesce, +} + +/// The four operators as the parser names them: `a + b` arrives as the call `__add(a, b)`. +#[derive(Clone, Copy)] +pub(super) enum Arithmetic { + Add, + Sub, + Mul, + Div, +} + +/// The scalar function of this name, if it is one. +pub(super) fn scalar(name: &str) -> Option { + [ + ("__add", Scalar::Arithmetic(Arithmetic::Add)), + ("__sub", Scalar::Arithmetic(Arithmetic::Sub)), + ("__mul", Scalar::Arithmetic(Arithmetic::Mul)), + ("__div", Scalar::Arithmetic(Arithmetic::Div)), + ("now", Scalar::Now), + ("coalesce", Scalar::Coalesce), + ] + .into_iter() + .find(|(known, _)| name.eq_ignore_ascii_case(known)) + .map(|(_, scalar)| scalar) +} + +/// Arithmetic as standard SQL does it: NULL in gives NULL out, two whole numbers give a whole +/// number or are refused when it would not fit, and a REAL on either side gives a REAL. +/// Dividing by zero is refused. +pub(super) fn arithmetic(op: Arithmetic, left: &Value, right: &Value) -> Result { + let by_zero = || Error::PlanError("division by zero".to_owned()); + Ok(match (left, right) { + (Value::Null, _) | (_, Value::Null) => Value::Null, + (Value::Int64(left), Value::Int64(right)) => Value::Int64( + match op { + Arithmetic::Add => left.checked_add(*right), + Arithmetic::Sub => left.checked_sub(*right), + Arithmetic::Mul => left.checked_mul(*right), + Arithmetic::Div if *right == 0 => return Err(by_zero()), + Arithmetic::Div => left.checked_div(*right), + } + .ok_or_else(out_of_range)?, + ), + _ => { + let (Some(left), Some(right)) = (real(left), real(right)) else { + return Err(Error::PlanError( + "arithmetic needs numbers on both sides".to_owned(), + )); + }; + Value::Float64(match op { + Arithmetic::Add => left + right, + Arithmetic::Sub => left - right, + Arithmetic::Mul => left * right, + Arithmetic::Div if right == 0.0 => return Err(by_zero()), + Arithmetic::Div => left / right, + }) + } + }) +} + +fn real(value: &Value) -> Option { + match value { + Value::Int64(value) => Some(*value as f64), + Value::Float64(value) => Some(*value), + _ => None, + } +} + +/// TRUE or FALSE, or `None` for NULL and for anything that is not a boolean. +fn truth(value: &Value) -> Option { + match value { + Value::Bool(value) => Some(*value), + _ => None, + } +} + +/// How two values compare, or `None` when either is NULL or their types do not compare. +/// +/// A REAL has one rule for `=`, ORDER BY and the bytes of a key: the two zeros are one value, +/// and every NaN is one value, equal to itself and above positive infinity. A whole number +/// compares with a REAL as a REAL, and a UUID with the text that spells one. +pub(super) fn compare(left: &Value, right: &Value) -> Option { + let reals = |left: f64, right: f64| { + let unordered = || left.is_nan().cmp(&right.is_nan()); + left.partial_cmp(&right).unwrap_or_else(unordered) + }; + Some(match (left, right) { + (Value::Int64(left), Value::Int64(right)) => left.cmp(right), + (Value::Float64(left), Value::Float64(right)) => reals(*left, *right), + (Value::Int64(left), Value::Float64(right)) => reals(*left as f64, *right), + (Value::Float64(left), Value::Int64(right)) => reals(*left, *right as f64), + (Value::Text(left), Value::Text(right)) => left.cmp(right), + (Value::Bool(left), Value::Bool(right)) => left.cmp(right), + (Value::Timestamp(left), Value::Timestamp(right)) => left.cmp(right), + (Value::TxId(left), Value::TxId(right)) => left.cmp(right), + (Value::Uuid(left), Value::Uuid(right)) => left.cmp(right), + (Value::Uuid(left), Value::Text(right)) => left.cmp(&right.parse().ok()?), + (Value::Text(left), Value::Uuid(right)) => left.parse::().ok()?.cmp(right), + _ => return None, + }) +} + +/// How two values sort going up: as they compare, with NULL after every value. Going down is +/// the reverse, which puts NULL first. +pub(super) fn sort_order(left: &Value, right: &Value) -> Ordering { + match (left, right) { + (Value::Null, Value::Null) => Ordering::Equal, + (Value::Null, _) => Ordering::Greater, + (_, Value::Null) => Ordering::Less, + _ => compare(left, right).unwrap_or(Ordering::Equal), + } +} diff --git a/v2/contextdb/src/query/mod.rs b/v2/contextdb/src/query/mod.rs new file mode 100644 index 000000000..414a7587a --- /dev/null +++ b/v2/contextdb/src/query/mod.rs @@ -0,0 +1,538 @@ +//! query: answers a SELECT from one table: which rows to read and through which index, the +//! filter, the select list, COUNT, SUM, MIN and MAX, ORDER BY and LIMIT. It holds the one +//! expression evaluator, and counts exactly the rows it reads. +//! +//! It never changes anything. + +mod access; +mod expression; +#[cfg(test)] +mod tests; + +use std::cmp::Ordering; +use std::collections::HashMap; +use std::fmt; + +pub(crate) use expression::eval; + +use crate::memory::{Budget, Reservation}; +use crate::schema::{Catalog, Table}; +use crate::sql::ast::SortDirection; +use crate::sql::plan::{ProjectColumn, SortKey}; +use crate::sql::{Expr, Plan}; +use crate::store::{Key, Read}; +use crate::types::{ColumnType, Error, Result, Value}; +use access::Source; +use expression::{ + Arithmetic, arithmetic, column_index, compare, most_bytes, reads, sort_order, unsupported, +}; + +/// What a statement returned: the column names and rows of a SELECT, or the number of rows a +/// write affected. Its rows stay counted against the memory limit until it is dropped. +pub struct QueryResult { + columns: Vec, + rows: Vec>, + rows_affected: u64, + rows_examined: u64, + /// Never read: held so that the memory the rows take stays counted against the limit, and + /// dropping it with the result gives that memory back. + _reservation: Option, +} + +impl fmt::Debug for QueryResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("QueryResult") + .field("columns", &self.columns) + .field("rows", &self.rows) + .field("rows_affected", &self.rows_affected) + .field("rows_examined", &self.rows_examined) + .finish_non_exhaustive() + } +} + +impl QueryResult { + /// The column names of a SELECT, in order; empty for a write. + pub fn columns(&self) -> &[String] { + &self.columns + } + + /// The rows of a SELECT, each a list of values in column order; empty for a write. + pub fn rows(&self) -> &[Vec] { + &self.rows + } + + /// How many rows a write changed; zero for a SELECT. + pub fn rows_affected(&self) -> u64 { + self.rows_affected + } + + /// How many stored rows the statement read to produce this result. An exact count, the same + /// on every run over the same store: a whole-table `COUNT(*)` reads none, a lookup by key + /// reads one. Speed is guarded in tests by this number, never by a time. + pub fn rows_examined(&self) -> u64 { + self.rows_examined + } + + /// The result of a write that changed `rows` rows. + pub(crate) fn affected(rows: u64) -> QueryResult { + QueryResult { + rows_affected: rows, + ..QueryResult::from_rows(Vec::new(), Vec::new()) + } + } + + /// The result of a statement its caller answers itself, such as `SHOW MEMORY_LIMIT`: these + /// columns and rows, with no row affected and none examined. + pub(crate) fn from_rows(columns: Vec, rows: Vec>) -> QueryResult { + QueryResult { + columns, + rows, + rows_affected: 0, + rows_examined: 0, + _reservation: None, + } + } +} + +/// What `SHOW MEMORY_LIMIT` answers for one budget, a writer's or a reading session's. +/// +/// One row: `limit`, `used` and `available` in bytes; `startup_ceiling`, the limit the writer or +/// the session was opened under; and `ceiling_refresh_failures`, which is always 0 because this +/// engine reads no ceiling from the operating system. No limit is the text `none`, and a number +/// of bytes larger than an INTEGER column holds is shown as the largest it holds. +pub(crate) fn memory_limit_shown(budget: &Budget, opened_under: Option) -> QueryResult { + let bytes = |bytes: Option| match bytes { + Some(bytes) => Value::Int64(i64::try_from(bytes).unwrap_or(i64::MAX)), + None => Value::Text("none".to_owned()), + }; + let (limit, used) = (budget.limit(), budget.used()); + let columns = [ + "limit", + "used", + "available", + "startup_ceiling", + "ceiling_refresh_failures", + ]; + let row = vec![ + bytes(limit), + bytes(Some(used)), + bytes(limit.map(|limit| limit.saturating_sub(used))), + bytes(opened_under), + Value::Int64(0), + ]; + QueryResult::from_rows(columns.map(str::to_owned).to_vec(), vec![row]) +} + +/// Answers one planned SELECT from one reader. +/// +/// Picks an index when the plan's filter can use one and scans otherwise; `COUNT(*)` of a whole +/// table is answered from the kept count. Loads a value kept beside its row only when a selected +/// or filtered column needs it. The result, and any sort or grouping on the way to it, grows +/// through reservations on `budget` and refuses with `MemoryBudgetExceeded` before it grows. +pub(crate) fn run( + plan: &Plan, + params: &HashMap, + catalog: &Catalog, + read: &dyn Read, + budget: &Budget, + now: i64, +) -> Result { + let select = Select::of(plan)?; + let table = catalog + .table(select.table) + .ok_or_else(|| Error::TableNotFound(select.table.to_owned()))?; + + // Everything the statement names is looked up before a row is read. + let width = table.meta.columns.len(); + let (mut filtered, mut wanted) = (vec![false; width], vec![false; width]); + if let Some(filter) = select.filter { + reads(filter, table, params, &mut filtered)?; + } + for key in select.order { + reads(&key.expr, table, params, &mut wanted)?; + } + let mut aggregates = Vec::new(); + let columns = match select.columns { + None => { + wanted.fill(true); + let declared = table.meta.columns.iter(); + declared.map(|column| column.name.clone()).collect() + } + Some(columns) => { + for column in columns { + let Some(aggregate) = Aggregate::of(&column.expr)? else { + reads(&column.expr, table, params, &mut wanted)?; + continue; + }; + if let Some(of) = aggregate.of { + reads(of, table, params, &mut wanted)?; + aggregate.check(table, of)?; + } + aggregates.push(aggregate); + } + if !aggregates.is_empty() && aggregates.len() < columns.len() { + return Err(unsupported("a column beside an aggregate (GROUP BY)")); + } + columns + .iter() + .map(|column| match (&column.alias, &column.expr) { + (Some(alias), _) => alias.clone(), + (None, Expr::Column(column)) => column.column.clone(), + (None, Expr::FunctionCall { name, .. }) if !aggregates.is_empty() => { + name.clone() + } + (None, _) => "expr".to_owned(), + }) + .collect() + } + }; + + let source = Source::new(table, select.filter, params, now, read, filtered, &wanted); + let limit = select.limit.map_or(usize::MAX, |limit| { + usize::try_from(limit).unwrap_or(usize::MAX) + }); + let mut reservation = budget.reserve(0, "query")?; + let mut rows = Vec::new(); + let mut examined = 0; + + if !aggregates.is_empty() { + if select.filter.is_none() && aggregates.iter().all(|aggregate| aggregate.of.is_none()) { + let count = read.count(table.id)? as i64; + for aggregate in &mut aggregates { + aggregate.seen = count; + } + } else { + let (path, _) = source.choose(&[])?; + examined = source.walk(&path, &mut reservation, |_, row, bytes, reservation| { + for aggregate in &mut aggregates { + aggregate.add(table, &row, params, now, reservation)?; + } + reservation.shrink(bytes); + Ok(true) + })?; + } + if limit > 0 { + // What a least or greatest value takes is already held. + reservation.grow(row_overhead(aggregates.len()))?; + rows.push(aggregates.into_iter().map(Aggregate::answer).collect()); + } + } else if limit > 0 { + // The select list worked out for one row. The memory of its values is asked for before + // they are made, and the row's bytes are given back once they are. + let output = + |row: Vec, bytes: u64, reservation: &mut Reservation| -> Result> { + let Some(columns) = select.columns else { + return Ok(row); + }; + let most = columns + .iter() + .map(|column| most_bytes(&column.expr, table, &row, params)); + reservation.grow(row_overhead(columns.len()) + most.sum::())?; + let values = columns + .iter() + .map(|column| eval(&column.expr, table, &row, params, now)) + .collect::>>()?; + reservation.shrink(bytes); + Ok(values) + }; + let (path, in_order) = source.choose(select.order)?; + if in_order { + examined = source.walk(&path, &mut reservation, |_, row, bytes, reservation| { + rows.push(output(row, bytes, reservation)?); + Ok(rows.len() < limit) + })?; + } else { + let mut sorting: Vec = Vec::new(); + let mut key_bytes = 0; + examined = source.walk(&path, &mut reservation, |_, row, bytes, reservation| { + let order = select.order; + let most = order + .iter() + .map(|key| most_bytes(&key.expr, table, &row, params)); + let held = row_overhead(order.len()) + most.sum::(); + reservation.grow(held + size_of::() as u64)?; + key_bytes += held + size_of::() as u64; + let keys = order + .iter() + .map(|key| eval(&key.expr, table, &row, params, now)) + .collect::>>()?; + sorting.push(Sorting { keys, row, bytes }); + Ok(true) + })?; + sorting.sort_by(|a, b| { + let keys = a.keys.iter().zip(&b.keys).zip(select.order); + keys.map(|((a, b), key)| match key.direction { + SortDirection::Desc => sort_order(a, b).reverse(), + _ => sort_order(a, b), + }) + .find(|order| order.is_ne()) + .unwrap_or(Ordering::Equal) + }); + for cut in sorting.drain(limit.min(sorting.len())..) { + reservation.shrink(cut.bytes); + } + for kept in sorting { + rows.push(output(kept.row, kept.bytes, &mut reservation)?); + } + reservation.shrink(key_bytes); + } + } + + Ok(QueryResult { + columns, + rows, + rows_affected: 0, + rows_examined: examined, + _reservation: Some(reservation), + }) +} + +/// The rows [`matching`] found, each with its key. +pub(crate) type Matched = Vec<(Key, Vec)>; + +/// The rows of one table that pass a filter, with their keys: what an UPDATE or DELETE acts on. +/// Uses the same index choice and the same evaluator as [`run`]. The rows and their keys stay +/// counted against the memory limit for as long as the caller holds the reservation that comes +/// with them. +pub(crate) fn matching( + table: &Table, + filter: Option<&Expr>, + params: &HashMap, + read: &dyn Read, + budget: &Budget, + now: i64, +) -> Result<(Matched, Reservation)> { + let width = table.meta.columns.len(); + let mut filtered = vec![false; width]; + if let Some(filter) = filter { + reads(filter, table, params, &mut filtered)?; + } + let source = Source::new( + table, + filter, + params, + now, + read, + filtered, + &vec![true; width], + ); + let (path, _) = source.choose(&[])?; + let mut reservation = budget.reserve(0, "query")?; + let mut found = Vec::new(); + let key_columns = source.key_columns(); + source.walk(&path, &mut reservation, |key, row, _, reservation| { + // A key is its row's key values once more, and is asked for as what they take. + let values = key_columns.iter().map(|at| heap_bytes(&row[*at])); + reservation.grow(row_overhead(key_columns.len()) + values.sum::())?; + found.push((key, row)); + Ok(true) + })?; + Ok((found, reservation)) +} + +/// One planned SELECT, as the planner nests it: LIMIT over the select list over ORDER BY over +/// one table with its filter. +struct Select<'a> { + table: &'a str, + filter: Option<&'a Expr>, + order: &'a [SortKey], + /// `None` for `SELECT *`. + columns: Option<&'a [ProjectColumn]>, + limit: Option, +} + +impl<'a> Select<'a> { + /// Reads the shape out of a plan, refusing by name a plan this version does not answer. + fn of(mut plan: &'a Plan) -> Result> { + let (mut limit, mut columns) = (None, None); + let mut order: &[SortKey] = &[]; + if let Plan::Limit { input, count } = plan { + limit = Some(*count); + plan = &**input; + } + if let Plan::Project { + input, + columns: list, + } = plan + { + columns = Some(list.as_slice()); + plan = &**input; + } + if let Plan::Sort { input, keys } = plan { + order = keys; + plan = &**input; + } + let Plan::Scan { table, filter } = plan else { + return Err(unsupported("this statement")); + }; + let (table, filter) = (table.as_str(), filter.as_ref()); + Ok(Select { + table, + filter, + order, + columns, + limit, + }) + } +} + +/// A row waiting to be sorted: the values it sorts by, and the bytes the row holds of the +/// statement's reservation. +struct Sorting { + keys: Vec, + row: Vec, + bytes: u64, +} + +/// One aggregate of a select list, and its answer over the rows seen so far. +struct Aggregate<'a> { + function: Function, + /// What is counted, added or compared; `None` for `COUNT(*)`. + of: Option<&'a Expr>, + /// How many rows took part: every row for `COUNT(*)`, otherwise those where `of` is not NULL. + seen: i64, + /// The sum, the least or the greatest so far; NULL until a row takes part. + value: Value, + /// The bytes of the statement's reservation that `value` holds. + held: u64, +} + +#[derive(Clone, Copy, PartialEq)] +enum Function { + Count, + Sum, + Min, + Max, +} + +impl<'a> Aggregate<'a> { + /// The aggregate an item of a select list is, if it is one. + fn of(expr: &'a Expr) -> Result>> { + let Expr::FunctionCall { name, args } = expr else { + return Ok(None); + }; + let functions = [ + ("COUNT", Function::Count), + ("SUM", Function::Sum), + ("MIN", Function::Min), + ("MAX", Function::Max), + ]; + let Some((_, function)) = functions + .into_iter() + .find(|(known, _)| name.eq_ignore_ascii_case(known)) + else { + return Ok(None); + }; + let [of] = args.as_slice() else { + return Err(Error::PlanError(format!("{name} takes one argument"))); + }; + let every_row = function == Function::Count + && matches!(of, Expr::Column(column) if column.column == "*"); + Ok(Some(Aggregate { + function, + of: (!every_row).then_some(of), + seen: 0, + value: Value::Null, + held: 0, + })) + } + + /// Refuses a SUM over a column that does not hold numbers, whatever rows the table has. + fn check(&self, table: &Table, of: &Expr) -> Result<()> { + let Expr::Column(column) = of else { + return Ok(()); + }; + let column = &table.meta.columns[column_index(table, column)?]; + if self.function == Function::Sum + && !matches!(column.column_type, ColumnType::Integer | ColumnType::Real) + { + return Err(Error::PlanError(format!( + "SUM needs numbers, and the column {} holds none", + column.name + ))); + } + Ok(()) + } + + /// Takes one row into the answer. A NULL takes no part in any aggregate but `COUNT(*)`. + /// The memory of the value is asked for before it is made, and given back unless the value + /// is kept as the least or the greatest so far. + fn add( + &mut self, + table: &Table, + row: &[Value], + params: &HashMap, + now: i64, + reservation: &mut Reservation, + ) -> Result<()> { + if let Some(of) = self.of { + let most = most_bytes(of, table, row, params); + reservation.grow(most)?; + let value = eval(of, table, row, params, now)?; + if value == Value::Null { + reservation.shrink(most); + return Ok(()); + } + let first = self.value == Value::Null; + let better = match self.function { + Function::Count => None, + Function::Sum => { + let so_far = if first { &Value::Int64(0) } else { &self.value }; + self.value = arithmetic(Arithmetic::Add, so_far, &value)?; + None + } + Function::Min => Some(Ordering::Less), + Function::Max => Some(Ordering::Greater), + }; + if better.is_some_and(|better| first || compare(&value, &self.value) == Some(better)) { + reservation.shrink(self.held); + (self.value, self.held) = (value, most); + } else { + reservation.shrink(most); + } + } + self.seen += 1; + Ok(()) + } + + /// The answer over the rows taken: a count of nothing is zero, and every other aggregate of + /// nothing is NULL. + fn answer(self) -> Value { + match self.function { + Function::Count => Value::Int64(self.seen), + _ => self.value, + } + } +} + +/// The memory a row of this many values takes before any text, JSON or vector in it. +fn row_overhead(values: usize) -> u64 { + (size_of::>() + values * size_of::()) as u64 +} + +/// The memory a value takes outside its own cell. +fn heap_bytes(value: &Value) -> u64 { + fn json(value: &serde_json::Value) -> u64 { + let cell = size_of::(); + match value { + serde_json::Value::String(text) => text.len() as u64, + serde_json::Value::Array(items) => { + let items = items.iter(); + items.map(|item| cell as u64 + json(item)).sum() + } + serde_json::Value::Object(members) => { + let members = members.iter(); + members + .map(|(name, member)| (name.len() + cell) as u64 + json(member)) + .sum() + } + _ => 0, + } + } + match value { + Value::Text(text) => text.len() as u64, + Value::Vector(vector) => size_of_val(vector.as_slice()) as u64, + Value::Json(value) => json(value), + _ => 0, + } +} diff --git a/v2/contextdb/src/query/tests.rs b/v2/contextdb/src/query/tests.rs new file mode 100644 index 000000000..19290c4c8 --- /dev/null +++ b/v2/contextdb/src/query/tests.rs @@ -0,0 +1,131 @@ +use std::cmp::Ordering; +use std::collections::HashMap; + +use super::expression::{Arithmetic, arithmetic, compare, eval, sort_order}; +use crate::schema::Table; +use crate::sql::Expr; +use crate::sql::ast::{BinOp, Literal, UnaryOp}; +use crate::store::TableId; +use crate::types::{Error, TableMeta, Uuid, Value}; + +/// An expression that reads no column, worked out. +fn worked_out(expr: &Expr) -> Value { + let table = Table { + id: TableId(1), + name: "t".to_owned(), + meta: TableMeta { + columns: Vec::new(), + unique_constraints: Vec::new(), + indexes: Vec::new(), + }, + index_ids: Vec::new(), + }; + eval(expr, &table, &[], &HashMap::new(), 0).expect("an expression of literals") +} + +fn joined(left: Literal, op: BinOp, right: Literal) -> Expr { + Expr::BinaryOp { + left: Box::new(Expr::Literal(left)), + op, + right: Box::new(Expr::Literal(right)), + } +} + +#[test] +fn null_is_unknown_unless_the_other_side_settles_the_answer() { + let (null, yes, no) = (Literal::Null, Literal::Bool(true), Literal::Bool(false)); + let and = |left: &Literal, right: &Literal| { + worked_out(&joined(left.clone(), BinOp::And, right.clone())) + }; + let or = |left: &Literal, right: &Literal| { + worked_out(&joined(left.clone(), BinOp::Or, right.clone())) + }; + assert_eq!(and(&null, &no), Value::Bool(false)); + assert_eq!(and(&no, &null), Value::Bool(false)); + assert_eq!(and(&null, &yes), Value::Null); + assert_eq!(and(&yes, &yes), Value::Bool(true)); + assert_eq!(or(&null, &yes), Value::Bool(true)); + assert_eq!(or(&yes, &null), Value::Bool(true)); + assert_eq!(or(&null, &no), Value::Null); + assert_eq!(or(&no, &no), Value::Bool(false)); + + let not_null = Expr::UnaryOp { + op: UnaryOp::Not, + operand: Box::new(Expr::Literal(Literal::Null)), + }; + assert_eq!(worked_out(¬_null), Value::Null); + let null_equals_null = joined(Literal::Null, BinOp::Eq, Literal::Null); + assert_eq!(worked_out(&null_equals_null), Value::Null); +} + +#[test] +fn arithmetic_keeps_whole_numbers_whole_and_refuses_what_does_not_fit() { + let (int, real) = (Value::Int64, Value::Float64); + let ok = |op, left: &Value, right: &Value| arithmetic(op, left, right).expect("arithmetic"); + assert_eq!(ok(Arithmetic::Div, &int(7), &int(2)), int(3)); + assert_eq!(ok(Arithmetic::Mul, &int(7), &real(0.5)), real(3.5)); + assert_eq!(ok(Arithmetic::Sub, &real(1.5), &int(1)), real(0.5)); + assert_eq!(ok(Arithmetic::Add, &Value::Null, &int(1)), Value::Null); + for (op, left, right) in [ + (Arithmetic::Add, int(i64::MAX), int(1)), + (Arithmetic::Mul, int(i64::MIN), int(-1)), + (Arithmetic::Div, int(7), int(0)), + (Arithmetic::Div, real(7.0), real(0.0)), + (Arithmetic::Add, int(7), Value::Text("x".to_owned())), + ] { + let refused = arithmetic(op, &left, &right); + assert!( + matches!(refused, Err(Error::PlanError(_))), + "{left:?} and {right:?} gave {refused:?}" + ); + } +} + +#[test] +fn values_sort_with_nan_above_every_number_and_null_after_everything() { + let mut values = [ + Value::Null, + Value::Float64(f64::NAN), + Value::Float64(f64::MAX), + Value::Int64(3), + Value::Float64(-1.5), + ]; + values.sort_by(sort_order); + assert_eq!( + values[..3], + [ + Value::Float64(-1.5), + Value::Int64(3), + Value::Float64(f64::MAX) + ] + ); + assert!(matches!(values[3], Value::Float64(nan) if nan.is_nan())); + assert_eq!(values[4], Value::Null); +} + +#[test] +fn a_uuid_compares_with_the_text_that_spells_it() { + let uuid = Value::Uuid(Uuid::from_u128(0x550e_8400_e29b_41d4_a716_4466_5544_0000)); + let spelled = |text: &str| Value::Text(text.to_owned()); + assert_eq!( + compare(&uuid, &spelled("550E8400-E29B-41D4-A716-446655440000")), + Some(Ordering::Equal) + ); + assert_eq!(compare(&spelled("not a uuid"), &uuid), None); + assert_eq!(compare(&uuid, &Value::Int64(1)), None); +} + +#[test] +fn a_real_compares_as_a_key_holds_it() { + let real = Value::Float64; + assert_eq!(compare(&real(-0.0), &real(0.0)), Some(Ordering::Equal)); + assert_eq!( + compare(&real(f64::NAN), &real(-f64::NAN)), + Some(Ordering::Equal) + ); + assert_eq!( + compare(&real(-f64::NAN), &real(f64::INFINITY)), + Some(Ordering::Greater) + ); + assert_eq!(compare(&Value::Int64(2), &real(2.5)), Some(Ordering::Less)); +} diff --git a/v2/contextdb/src/read/mod.rs b/v2/contextdb/src/read/mod.rs new file mode 100644 index 000000000..0cc5d7f38 --- /dev/null +++ b/v2/contextdb/src/read/mod.rs @@ -0,0 +1,185 @@ +//! read: reading a store without holding it for writing, from the writer's process or any +//! other. A session opens the folder itself, and each statement reads the newest commit under +//! the session's own memory limit. +//! +//! It never writes to the store itself. + +use std::collections::HashMap; +use std::path::Path; +use std::sync::{Arc, Mutex, PoisonError}; + +use crate::memory::Budget; +use crate::query::{self, QueryResult}; +use crate::schema::{self, Catalog}; +use crate::sql::{self, Effect, Plan}; +use crate::store::{Store, View, WrapStorage}; +use crate::system::{self, Clock}; +use crate::types::{Error, Result, TxId, Value}; + +/// What a reader may say when it opens a store. +/// +/// Start from `ReadSessionOptions::default()` and assign the fields to change. The struct is +/// `#[non_exhaustive]`, so outside this crate it cannot be written out field by field. +#[derive(Clone, Default)] +#[non_exhaustive] +pub struct ReadSessionOptions { + /// The most memory, in bytes, the session may use for its page cache and for the results + /// it has returned and that are still alive. `None`, the default, is no ceiling: a reader + /// does not take the limit the store remembers for its writer, so a reader that needs a + /// cap sets it here. Every open sets aside 8 MiB for the page cache and 1 MiB for the + /// small fixed needs of a statement, so a limit under 9 MiB is refused at open with + /// [`crate::Error::MemoryBudgetExceeded`]. + pub memory_limit: Option, + /// Where the session gets the time that `NOW()` in a statement answers with. `None`, the + /// default, is the machine's clock. + pub clock: Option>, + /// A wrapper around the data file the session has opened; the session then reads the file + /// only through what the wrapper returns. `None`, the default, reads the file as it is. + pub storage: Option, +} + +/// A store opened for reading. +/// +/// A session is opened by the folder's path, with [`ReadSession::open`]; no call on +/// [`crate::Database`] returns one. It reads the folder itself and needs nothing from the +/// writer's process: it opens whether or not a writer is running, never makes a writer fail, +/// holds one up only for the instant a call takes to note which commit it reads, and keeps +/// answering when the writer stops, is killed or starts again. It changes nothing a writer +/// stored. While a call runs it keeps one small file under `readers/` in the +/// folder, which is how a writer knows the pages being read are still needed. +/// +/// Each call sees the newest commit as of the moment it starts and keeps that one moment until +/// it returns. The next call starts from the newest commit again: no call holds one commit +/// across two statements. +/// +/// Which to read with: the process that holds the store for writing reads through its +/// [`crate::Database`], where a transaction's statements also see what the transaction has +/// written. Every other process reads through a `ReadSession`, and so does code that must not +/// be able to write: a session refuses a statement that would change something with +/// [`crate::Error::ReadOnlySession`]. +/// +/// Calls block and may be made from many threads. +pub struct ReadSession { + store: Store, + budget: Budget, + clock: Arc, + /// The tables as last read, with the schema commit they were read at. + catalog: Mutex)>>, +} + +impl ReadSession { + /// Opens the store in this folder for reading, with no memory limit and the machine's + /// clock. Never creates anything. + /// + /// # Errors + /// + /// - [`crate::Error::StoreMissing`] when there is no store at the path. + /// - [`crate::Error::StoreIo`] when the folder or its data file cannot be opened or read. + /// - [`crate::Error::StoreCorrupted`] when the data file fails its check. + pub fn open(path: impl AsRef) -> Result { + ReadSession::open_with_options(path, ReadSessionOptions::default()) + } + + /// [`ReadSession::open`] with options. + /// + /// # Errors + /// + /// Those of [`ReadSession::open`], and [`crate::Error::MemoryBudgetExceeded`] when + /// [`ReadSessionOptions::memory_limit`] is under the 9 MiB every open sets aside. + pub fn open_with_options( + path: impl AsRef, + options: ReadSessionOptions, + ) -> Result { + let budget = Budget::new(options.memory_limit.map(|bytes| bytes as u64))?; + let store = Store::open_for_reading(path.as_ref(), &budget, options.storage.as_ref())?; + Ok(ReadSession { + store, + budget, + clock: options.clock.unwrap_or_else(system::machine_clock), + catalog: Mutex::new(None), + }) + } + + /// Runs one statement that reads, `SELECT` or `SHOW MEMORY_LIMIT`, and returns its result. + /// + /// `params` gives a value for each `$name` in the statement; [`crate::params!`] writes the + /// map. + /// + /// # Errors + /// + /// - [`crate::Error::ReadOnlySession`] when the statement would declare a table or an + /// index, write rows or change the memory limit. Nothing ran; run it on a + /// [`crate::Database`]. + /// - [`crate::Error::ParseError`] or [`crate::Error::PlanError`] when the text is not a + /// statement this version runs, in the words a [`crate::Database`] refuses it with. + /// - [`crate::Error::TableNotFound`] when the newest commit has no table of that name. + /// - [`crate::Error::MemoryBudgetExceeded`] when the result would pass the session's + /// memory limit. The session stays usable. + /// - [`crate::Error::StoreIo`] when the folder or its data file cannot be read, and + /// [`crate::Error::StoreCorrupted`] when what was read fails its check. + pub fn execute(&self, sql: &str, params: &HashMap) -> Result { + let plan = sql::plan(&sql::parse(sql)?)?; + if plan.effect() != Effect::Reads { + return Err(Error::ReadOnlySession); + } + if let Plan::ShowMemoryLimit = plan { + // Nothing changes a session's limit, so the one it has is the one it opened under. + let opened_under = self.budget.limit(); + return Ok(query::memory_limit_shown(&self.budget, opened_under)); + } + let now = system::now_millis(&*self.clock); + let (view, catalog) = self.newest()?; + query::run(&plan, params, &catalog, &view, &self.budget, now) + } + + /// The names of the store's tables as of the newest commit, in name order. A store with no + /// table gives an empty list. + /// + /// # Errors + /// + /// This call reads the folder, so unlike [`crate::Database::table_names`] it can fail: + /// [`crate::Error::StoreIo`] when the folder or its data file cannot be read, and + /// [`crate::Error::StoreCorrupted`] when what was read fails its check. + pub fn table_names(&self) -> Result> { + let (_, catalog) = self.newest()?; + let tables = catalog.tables().into_iter(); + Ok(tables.map(|table| table.name.clone()).collect()) + } + + /// A table's declaration as SQL text, as of the newest commit: its `CREATE TABLE` statement, + /// then one `CREATE INDEX` statement for each index made with `CREATE INDEX`, each ended + /// by `;` and a new line. A clause left at its default is not written. It is the text + /// [`crate::Database::declaration`] gives, and running it on an empty store declares the + /// same table. + /// + /// # Errors + /// + /// - [`crate::Error::TableNotFound`] when the newest commit has no table of this name. + /// - [`crate::Error::StoreIo`] when the folder or its data file cannot be read, and + /// [`crate::Error::StoreCorrupted`] when what was read fails its check. + pub fn declaration(&self, table: &str) -> Result { + let (_, catalog) = self.newest()?; + match catalog.table(table) { + Some(table) => Ok(schema::render(table)), + None => Err(Error::TableNotFound(table.to_owned())), + } + } + + /// The newest commit and the tables declared at it. The view holds that commit's pages for + /// as long as the caller keeps it; the tables are read again only when a declaration has + /// been committed since they were last read. + fn newest(&self) -> Result<(View, Arc)> { + let view = self.store.view()?; + let declared_at = view.schema_commit()?; + // The lock guards a finished catalog or none, so a holder that panicked left it whole. + let mut kept = self.catalog.lock().unwrap_or_else(PoisonError::into_inner); + if let Some((commit, catalog)) = &*kept + && *commit == declared_at + { + return Ok((view, catalog.clone())); + } + let catalog = Arc::new(Catalog::load(&view)?); + *kept = Some((declared_at, catalog.clone())); + Ok((view, catalog)) + } +} diff --git a/v2/contextdb/src/schema/check.rs b/v2/contextdb/src/schema/check.rs new file mode 100644 index 000000000..4fff4efd9 --- /dev/null +++ b/v2/contextdb/src/schema/check.rs @@ -0,0 +1,220 @@ +//! Checking a declaration: what CREATE TABLE and CREATE INDEX may say, and the table each gives. + +use super::{Catalog, RESERVED_PREFIXES, Table}; +use crate::sql::ast::{self, DataType, Expr, Literal, UnaryOp}; +use crate::sql::plan::{CreateIndexPlan, CreateTablePlan}; +use crate::store::{IndexId, TableId}; +use crate::types::{ + ColumnDef, ColumnDefault, ColumnType, Error, IndexDecl, Result, SortDirection, TableMeta, Value, +}; + +/// The refusal of a statement or clause that arrives with a later version. +pub(super) fn not_supported(clause: &str) -> Error { + Error::PlanError(format!( + "{clause} is not supported in this version of the library" + )) +} + +fn invalid(reason: String) -> Error { + Error::SchemaInvalid { reason } +} + +/// The table a CREATE TABLE declares, numbered after the tables the catalog holds. +pub(super) fn new_table(create: &CreateTablePlan, catalog: &Catalog) -> Result
{ + let table = &create.name; + let mut columns: Vec = Vec::with_capacity(create.columns.len()); + for declared in &create.columns { + if columns.iter().any(|earlier| earlier.name == declared.name) { + return Err(invalid(format!( + "table '{table}' declares the column '{}' twice", + declared.name + ))); + } + columns.push(column(table, declared)?); + } + match columns.iter().filter(|column| column.primary_key).count() { + 0 => return Err(not_supported("a table without a PRIMARY KEY")), + 1 => {} + _ => { + return Err(invalid(format!( + "table '{table}' declares more than one PRIMARY KEY" + ))); + } + } + + let mut meta = TableMeta { + columns, + unique_constraints: create.unique_constraints.clone(), + indexes: Vec::new(), + }; + // The index behind the key, then behind each UNIQUE column, then behind each UNIQUE rule. + // The key's index already keeps the key column unique. + let one = |column: &ColumnDef| vec![column.name.clone()]; + let key = meta.columns.iter().filter(|column| column.primary_key); + let unique = meta.columns.iter(); + let unique = unique.filter(|column| column.unique && !column.primary_key); + let rules = create.unique_constraints.iter().cloned(); + let behind = key + .map(|column| ("__pk_", one(column))) + .chain(unique.map(|column| ("__unique_", one(column)))) + .chain(rules.map(|rule| ("__unique_", rule))); + for (prefix, rule) in behind { + // Two rules can spell one name (`a_b`, and `a` with `b`); the later one takes a number. + let plain = format!("{prefix}{}", rule.join("_")); + let mut name = plain.clone(); + let mut copy = 1; + while meta.indexes.iter().any(|other| other.name == name) { + copy += 1; + name = format!("{plain}_{copy}"); + } + let columns = rule.into_iter().map(|column| (column, SortDirection::Asc)); + let index = IndexDecl { + name, + columns: columns.collect(), + }; + check_columns(table, &meta, &index, true)?; + meta.indexes.push(index); + } + + let id = catalog.tables.values().map(|other| other.id.0 + 1).max(); + let index_ids = (next_index_id(catalog)..) + .take(meta.indexes.len()) + .map(IndexId) + .collect(); + Ok(Table { + id: TableId(id.unwrap_or(0)), + name: table.clone(), + meta, + index_ids, + }) +} + +/// The table a CREATE INDEX names, with the new index as its last. +pub(super) fn with_index(create: &CreateIndexPlan, catalog: &Catalog) -> Result
{ + let reserved = RESERVED_PREFIXES + .into_iter() + .find(|prefix| create.name.starts_with(*prefix)); + if let Some(prefix) = reserved { + return Err(Error::ReservedIndexName { + table: create.table.clone(), + name: create.name.clone(), + prefix: prefix.to_owned(), + }); + } + let mut table = catalog + .table(&create.table) + .ok_or_else(|| Error::TableNotFound(create.table.clone()))? + .clone(); + let index = IndexDecl { + name: create.name.clone(), + columns: create.columns.clone(), + }; + check_columns(&table.name, &table.meta, &index, false)?; + if table + .meta + .indexes + .iter() + .any(|other| other.name == index.name) + { + return Err(Error::DuplicateIndex { + table: table.name, + index: index.name, + }); + } + table.meta.indexes.push(index); + table.index_ids.push(IndexId(next_index_id(catalog))); + Ok(table) +} + +/// One past the highest index number in use. Nothing in this version removes an index or a +/// table, so counting on from the highest never gives a number twice. +fn next_index_id(catalog: &Catalog) -> u32 { + let ids = catalog.tables.values().flat_map(|table| &table.index_ids); + ids.map(|id| id.0 + 1).max().unwrap_or(0) +} + +/// Refuses an index over a column the table does not have or whose type it cannot be over. +/// `rule` says the index is the one behind a key or a UNIQUE rule. +fn check_columns(table: &str, meta: &TableMeta, index: &IndexDecl, rule: bool) -> Result<()> { + for (name, _) in &index.columns { + let column = meta.columns.iter().find(|column| column.name == *name); + let column = column.ok_or_else(|| Error::ColumnNotFound { + table: table.to_owned(), + column: name.clone(), + })?; + // JSON and VECTOR have no order to keep an index by. A REAL has one, and is still no + // identity for a key or a UNIQUE rule: it has two zeros and a NaN. + let refused = match column.column_type { + ColumnType::Json | ColumnType::Vector(_) => true, + ColumnType::Real => rule, + _ => false, + }; + if refused { + return Err(Error::ColumnNotIndexable { + table: table.to_owned(), + column: name.clone(), + column_type: column.column_type, + }); + } + } + Ok(()) +} + +/// One column as the catalog keeps it. +fn column(table: &str, declared: &ast::ColumnDef) -> Result { + let column_type = match declared.data_type { + DataType::Integer => ColumnType::Integer, + DataType::Real => ColumnType::Real, + DataType::Text => ColumnType::Text, + DataType::Boolean => ColumnType::Boolean, + DataType::Uuid => ColumnType::Uuid, + DataType::Timestamp => ColumnType::Timestamp, + DataType::Json => ColumnType::Json, + DataType::Vector(size) => ColumnType::Vector(size as usize), + }; + let default = match &declared.default { + Some(expr) => Some(default(expr).ok_or_else(|| { + invalid(format!( + "the DEFAULT of '{table}.{}' must be NULL, TRUE, FALSE, a number, quoted text \ + or NOW()", + declared.name + )) + })?), + None => None, + }; + Ok(ColumnDef { + name: declared.name.clone(), + column_type, + nullable: declared.nullable, + primary_key: declared.primary_key, + unique: declared.unique, + default, + }) +} + +/// A DEFAULT as the catalog keeps it, in the type the author wrote. `None` for anything but +/// NULL, TRUE, FALSE, a number, quoted text or `NOW()`. +pub(super) fn default(expr: &Expr) -> Option { + let value = match expr { + Expr::FunctionCall { name, args } + if name.eq_ignore_ascii_case("NOW") && args.is_empty() => + { + return Some(ColumnDefault::Now); + } + Expr::Literal(Literal::Null) => Value::Null, + Expr::Literal(Literal::Bool(flag)) => Value::Bool(*flag), + Expr::Literal(Literal::Integer(whole)) => Value::Int64(*whole), + Expr::Literal(Literal::Real(real)) => Value::Float64(*real), + Expr::Literal(Literal::Text(words)) => Value::Text(words.clone()), + Expr::UnaryOp { + op: UnaryOp::Neg, + operand, + } => match **operand { + Expr::Literal(Literal::Integer(whole)) => Value::Int64(-whole), + Expr::Literal(Literal::Real(real)) => Value::Float64(-real), + _ => return None, + }, + _ => return None, + }; + Some(ColumnDefault::Literal(value)) +} diff --git a/v2/contextdb/src/schema/mod.rs b/v2/contextdb/src/schema/mod.rs new file mode 100644 index 000000000..0faace21c --- /dev/null +++ b/v2/contextdb/src/schema/mod.rs @@ -0,0 +1,101 @@ +//! schema: table declarations: checking them, storing them, printing them back. +//! +//! It never changes a row. +//! +//! A table's declaration is kept as one schema record under the table's name: its number, its +//! columns and rules as they were declared, and its indexes with their numbers. +//! +//! A table's indexes come in one order: the index behind the primary key (`__pk_`), +//! the index behind each UNIQUE column in column order (`__unique_`; the key column has +//! its key's index and no other), the index behind each UNIQUE rule of the table in declared +//! order (`__unique__...`), then the indexes of CREATE INDEX in the order they +//! were made. + +mod check; +mod record; +mod render; +#[cfg(test)] +mod tests; + +use std::collections::BTreeMap; + +use crate::sql::Plan; +use crate::store::{ChangeSet, IndexId, TableId, View}; +use crate::types::{Error, Result, TableMeta}; + +/// What the name of an index the engine makes for itself begins with. An index of CREATE INDEX +/// may not take one of these. +const RESERVED_PREFIXES: [&str; 3] = ["__pk_", "__unique_", "__fk_"]; + +/// One declared table as the other parts use it. +#[derive(Debug, Clone)] +pub(crate) struct Table { + pub id: TableId, + pub name: String, + pub meta: TableMeta, + /// One id per entry of `meta.indexes`, in the same order. + pub index_ids: Vec, +} + +/// Every table declared in a store at one moment. +pub(crate) struct Catalog { + tables: BTreeMap, +} + +impl Catalog { + /// Reads the declarations the reader shows. Costs one small record per table; `database` + /// keeps the result and loads again only when [`View::schema_commit`] has moved. + pub(crate) fn load(view: &View) -> Result { + let mut tables = BTreeMap::new(); + for bytes in view.schema_records()? { + let table = record::decode(&bytes); + tables.insert(table.name.clone(), table); + } + Ok(Catalog { tables }) + } + + /// A table by name, as SQL names it. + pub(crate) fn table(&self, name: &str) -> Option<&Table> { + self.tables.get(name) + } + + /// Every table, in name order. + pub(crate) fn tables(&self) -> Vec<&Table> { + self.tables.values().collect() + } +} + +/// Applies one declaration (CREATE TABLE, CREATE INDEX, ALTER TABLE, DROP) to a change set. +/// +/// Checks the declaration against the catalog, stages its schema record and returns the +/// catalog as it will be once the change set commits. Stages no row and no index entry; after +/// a CREATE INDEX on a table that has rows, `database` asks `write` to fill the index. +pub(crate) fn declare(plan: &Plan, catalog: &Catalog, changes: &mut ChangeSet) -> Result { + let mut tables = catalog.tables.clone(); + let table = match plan { + Plan::CreateTable(create) if !tables.contains_key(&create.name) => { + check::new_table(create, catalog)? + } + // A taken name is settled before the rest of the declaration is looked at. + Plan::CreateTable(create) if create.if_not_exists => return Ok(Catalog { tables }), + Plan::CreateTable(create) => { + return Err(Error::SchemaInvalid { + reason: format!( + "table '{}' already exists: CREATE TABLE never redefines a table, so nothing \ + was changed; CREATE TABLE IF NOT EXISTS leaves an existing table as it is", + create.name + ), + }); + } + Plan::CreateIndex(create) => check::with_index(create, catalog)?, + _ => return Err(check::not_supported("this declaration")), + }; + changes.put_schema(&table.name, record::encode(&table))?; + tables.insert(table.name.clone(), table); + Ok(Catalog { tables }) +} + +/// The declaration as `.schema` prints it: text that, run again, declares the same table. +pub(crate) fn render(table: &Table) -> String { + render::declaration(table) +} diff --git a/v2/contextdb/src/schema/record.rs b/v2/contextdb/src/schema/record.rs new file mode 100644 index 000000000..0650e5cd1 --- /dev/null +++ b/v2/contextdb/src/schema/record.rs @@ -0,0 +1,193 @@ +//! Storing a declaration: the bytes of a table's schema record, and the table read back from +//! them. +//! +//! Every number is four bytes, low byte first. A text is its length in bytes and then its +//! bytes. A list is its length and then its items. +//! +//! Reading trusts the bytes: a record is only ever what `encode` wrote, and the store checks +//! every page it reads. Where a byte says which of several kinds follows, the last kind is read +//! for any byte the earlier ones do not claim. + +use super::Table; +use crate::store::{IndexId, TableId}; +use crate::types::{ + ColumnDef, ColumnDefault, ColumnType, IndexDecl, SortDirection, TableMeta, Value, +}; + +/// The schema record of a table. +pub(super) fn encode(table: &Table) -> Vec { + let meta = &table.meta; + let mut out = Vec::new(); + number(&mut out, table.id.0 as usize); + text(&mut out, &table.name); + number(&mut out, meta.columns.len()); + for column in &meta.columns { + text(&mut out, &column.name); + let (kind, size) = match column.column_type { + ColumnType::Integer => (0, 0), + ColumnType::Real => (1, 0), + ColumnType::Text => (2, 0), + ColumnType::Boolean => (3, 0), + ColumnType::Json => (4, 0), + ColumnType::Uuid => (5, 0), + ColumnType::Timestamp => (6, 0), + ColumnType::Vector(size) => (8, size), + }; + out.push(kind); + number(&mut out, size); + out.push(u8::from(column.nullable)); + out.push(u8::from(column.primary_key)); + out.push(u8::from(column.unique)); + match &column.default { + None => out.push(0), + Some(ColumnDefault::Now) => out.push(1), + Some(ColumnDefault::Literal(Value::Null)) => out.push(2), + Some(ColumnDefault::Literal(Value::Bool(flag))) => out.extend([3, u8::from(*flag)]), + Some(ColumnDefault::Literal(Value::Int64(whole))) => { + out.push(4); + out.extend(whole.to_le_bytes()); + } + Some(ColumnDefault::Literal(Value::Float64(real))) => { + out.push(5); + out.extend(real.to_le_bytes()); + } + Some(ColumnDefault::Literal(Value::Text(words))) => { + out.push(6); + text(&mut out, words); + } + Some(ColumnDefault::Literal(other)) => { + unreachable!("a default is declared as a literal, never as {other:?}") + } + } + } + number(&mut out, meta.unique_constraints.len()); + for rule in &meta.unique_constraints { + number(&mut out, rule.len()); + for column in rule { + text(&mut out, column); + } + } + number(&mut out, meta.indexes.len()); + for (index, id) in meta.indexes.iter().zip(&table.index_ids) { + number(&mut out, id.0 as usize); + text(&mut out, &index.name); + number(&mut out, index.columns.len()); + for (column, direction) in &index.columns { + text(&mut out, column); + out.push(u8::from(*direction == SortDirection::Desc)); + } + } + out +} + +/// The table a schema record was written from. +pub(super) fn decode(record: &[u8]) -> Table { + let mut read = Reader(record); + let id = TableId(read.number() as u32); + let name = read.text(); + let columns = read.list(|read| { + let name = read.text(); + let kind = read.byte(); + let size = read.number(); + ColumnDef { + name, + column_type: match kind { + 0 => ColumnType::Integer, + 1 => ColumnType::Real, + 2 => ColumnType::Text, + 3 => ColumnType::Boolean, + 4 => ColumnType::Json, + 5 => ColumnType::Uuid, + 6 => ColumnType::Timestamp, + _ => ColumnType::Vector(size), + }, + nullable: read.flag(), + primary_key: read.flag(), + unique: read.flag(), + default: match read.byte() { + 0 => None, + 1 => Some(ColumnDefault::Now), + kind => Some(ColumnDefault::Literal(match kind { + 2 => Value::Null, + 3 => Value::Bool(read.flag()), + 4 => Value::Int64(i64::from_le_bytes(read.array())), + 5 => Value::Float64(f64::from_le_bytes(read.array())), + _ => Value::Text(read.text()), + })), + }, + } + }); + let unique_constraints = read.list(|read| read.list(Reader::text)); + let mut index_ids = Vec::new(); + let indexes = read.list(|read| { + index_ids.push(IndexId(read.number() as u32)); + let name = read.text(); + let columns = read.list(|read| { + let column = read.text(); + let direction = if read.flag() { + SortDirection::Desc + } else { + SortDirection::Asc + }; + (column, direction) + }); + IndexDecl { name, columns } + }); + Table { + id, + name, + meta: TableMeta { + columns, + unique_constraints, + indexes, + }, + index_ids, + } +} + +fn number(out: &mut Vec, number: usize) { + out.extend((number as u32).to_le_bytes()); +} + +fn text(out: &mut Vec, text: &str) { + number(out, text.len()); + out.extend(text.as_bytes()); +} + +/// What is left of a record being read. +struct Reader<'a>(&'a [u8]); + +impl<'a> Reader<'a> { + fn bytes(&mut self, count: usize) -> &'a [u8] { + let (taken, rest) = self.0.split_at(count); + self.0 = rest; + taken + } + + fn array(&mut self) -> [u8; N] { + let mut array = [0; N]; + array.copy_from_slice(self.bytes(N)); + array + } + + fn byte(&mut self) -> u8 { + self.bytes(1)[0] + } + + fn flag(&mut self) -> bool { + self.byte() != 0 + } + + fn number(&mut self) -> usize { + u32::from_le_bytes(self.array()) as usize + } + + fn text(&mut self) -> String { + let count = self.number(); + String::from_utf8_lossy(self.bytes(count)).into_owned() + } + + fn list(&mut self, mut item: impl FnMut(&mut Self) -> T) -> Vec { + (0..self.number()).map(|_| item(self)).collect() + } +} diff --git a/v2/contextdb/src/schema/render.rs b/v2/contextdb/src/schema/render.rs new file mode 100644 index 000000000..0e0ed6bd7 --- /dev/null +++ b/v2/contextdb/src/schema/render.rs @@ -0,0 +1,100 @@ +//! Printing a declaration back as SQL. + +use super::{RESERVED_PREFIXES, Table}; +use crate::types::{ColumnDef, ColumnDefault, ColumnType, SortDirection, Value}; + +/// A table's declaration as SQL: the CREATE TABLE, then one CREATE INDEX for each index the +/// author made, every statement ended by `;` and a new line. A clause left at its default is +/// not printed, and neither are the indexes the engine keeps behind a key or a UNIQUE rule. +pub(super) fn declaration(table: &Table) -> String { + let meta = &table.meta; + let mut lines: Vec = meta.columns.iter().map(column).collect(); + for rule in &meta.unique_constraints { + let columns: Vec = rule.iter().map(|column| name(column)).collect(); + lines.push(format!("UNIQUE ({})", columns.join(", "))); + } + let table = name(&table.name); + let mut text = format!("CREATE TABLE {table} (\n {}\n);\n", lines.join(",\n ")); + for index in &meta.indexes { + if RESERVED_PREFIXES + .iter() + .any(|prefix| index.name.starts_with(*prefix)) + { + continue; + } + let columns: Vec = index + .columns + .iter() + .map(|(column, direction)| match direction { + SortDirection::Asc => name(column), + SortDirection::Desc => format!("{} DESC", name(column)), + }) + .collect(); + text.push_str(&format!( + "CREATE INDEX {} ON {table} ({});\n", + name(&index.name), + columns.join(", ") + )); + } + text +} + +/// One column's line: its name, its type, and the clauses it was given. +fn column(column: &ColumnDef) -> String { + let mut text = name(&column.name); + text.push(' '); + match column.column_type { + ColumnType::Integer => text.push_str("INTEGER"), + ColumnType::Real => text.push_str("REAL"), + ColumnType::Text => text.push_str("TEXT"), + ColumnType::Boolean => text.push_str("BOOLEAN"), + ColumnType::Json => text.push_str("JSON"), + ColumnType::Uuid => text.push_str("UUID"), + ColumnType::Timestamp => text.push_str("TIMESTAMP"), + ColumnType::Vector(size) => text.push_str(&format!("VECTOR({size})")), + } + if column.primary_key { + text.push_str(" PRIMARY KEY"); + } + if !column.nullable { + text.push_str(" NOT NULL"); + } + match &column.default { + Some(ColumnDefault::Literal(value)) => { + text.push_str(" DEFAULT "); + text.push_str(&literal(value)); + } + Some(ColumnDefault::Now) => text.push_str(" DEFAULT NOW()"), + None => {} + } + if column.unique { + text.push_str(" UNIQUE"); + } + text +} + +/// A default's value as the author wrote it. +fn literal(value: &Value) -> String { + match value { + Value::Null => "NULL".to_owned(), + Value::Bool(true) => "TRUE".to_owned(), + Value::Bool(false) => "FALSE".to_owned(), + Value::Int64(whole) => whole.to_string(), + // A whole REAL keeps its decimal point, so it reads back as a REAL. + Value::Float64(real) if real.fract() == 0.0 => format!("{real:.1}"), + Value::Float64(real) => real.to_string(), + Value::Text(words) => format!("'{}'", words.replace('\'', "''")), + other => unreachable!("a default is declared as a literal, never as {other:?}"), + } +} + +/// A name as SQL reads it back: bare when it is one plain word, in double quotes otherwise. +fn name(name: &str) -> String { + let plain = name.starts_with(|first: char| first.is_ascii_alphabetic() || first == '_') + && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'); + if plain { + name.to_owned() + } else { + format!("\"{}\"", name.replace('"', "\"\"")) + } +} diff --git a/v2/contextdb/src/schema/tests.rs b/v2/contextdb/src/schema/tests.rs new file mode 100644 index 000000000..b648333bd --- /dev/null +++ b/v2/contextdb/src/schema/tests.rs @@ -0,0 +1,305 @@ +use std::collections::BTreeMap; + +use super::{Catalog, Table, check, record, render}; +use crate::sql::ast::{self, DataType, Expr, Literal, UnaryOp}; +use crate::sql::plan::{CreateIndexPlan, CreateTablePlan}; +use crate::store::{IndexId, TableId}; +use crate::types::{ + ColumnDef, ColumnDefault, ColumnType, Error, IndexDecl, SortDirection, TableMeta, Value, +}; + +fn column(name: &str, column_type: ColumnType) -> ColumnDef { + ColumnDef { + name: name.to_owned(), + column_type, + nullable: true, + primary_key: false, + unique: false, + default: None, + } +} + +fn index(name: &str, columns: &[(&str, SortDirection)]) -> IndexDecl { + IndexDecl { + name: name.to_owned(), + columns: columns + .iter() + .map(|(column, direction)| ((*column).to_owned(), *direction)) + .collect(), + } +} + +/// A table with every kind of clause this version keeps, as CREATE TABLE and two CREATE INDEX +/// statements leave it. +fn notes() -> Table { + use SortDirection::{Asc, Desc}; + let literal = |value| Some(ColumnDefault::Literal(value)); + Table { + id: TableId(4), + name: "notes".to_owned(), + meta: TableMeta { + columns: vec![ + ColumnDef { + primary_key: true, + ..column("id", ColumnType::Integer) + }, + ColumnDef { + nullable: false, + default: literal(Value::Text("it's".to_owned())), + ..column("title", ColumnType::Text) + }, + ColumnDef { + default: literal(Value::Null), + ..column("body", ColumnType::Text) + }, + ColumnDef { + default: literal(Value::Int64(3)), + ..column("stars", ColumnType::Integer) + }, + ColumnDef { + unique: true, + ..column("slug", ColumnType::Text) + }, + ColumnDef { + nullable: false, + default: Some(ColumnDefault::Now), + ..column("made at", ColumnType::Timestamp) + }, + ColumnDef { + default: literal(Value::Float64(-2.0)), + ..column("score", ColumnType::Real) + }, + ColumnDef { + default: literal(Value::Bool(true)), + ..column("kept", ColumnType::Boolean) + }, + column("embedding", ColumnType::Vector(384)), + ], + unique_constraints: vec![vec!["title".to_owned(), "stars".to_owned()]], + indexes: vec![ + index("__pk_id", &[("id", Asc)]), + index("__unique_slug", &[("slug", Asc)]), + index("__unique_title_stars", &[("title", Asc), ("stars", Asc)]), + index("by_stars", &[("stars", Desc), ("made at", Asc)]), + index("by_body", &[("body", Asc)]), + ], + }, + index_ids: [7, 8, 9, 12, 15].map(IndexId).to_vec(), + } +} + +#[test] +fn a_record_reads_back_as_the_table_it_was_written_from() { + let table = notes(); + let back = record::decode(&record::encode(&table)); + assert_eq!(back.id, table.id); + assert_eq!(back.name, table.name); + assert_eq!(back.meta, table.meta); + assert_eq!(back.index_ids, table.index_ids); +} + +#[test] +fn render_prints_what_the_author_declared_and_nothing_else() { + assert_eq!( + render(¬es()), + "\ +CREATE TABLE notes ( + id INTEGER PRIMARY KEY, + title TEXT NOT NULL DEFAULT 'it''s', + body TEXT DEFAULT NULL, + stars INTEGER DEFAULT 3, + slug TEXT UNIQUE, + \"made at\" TIMESTAMP NOT NULL DEFAULT NOW(), + score REAL DEFAULT -2.0, + kept BOOLEAN DEFAULT TRUE, + embedding VECTOR(384), + UNIQUE (title, stars) +); +CREATE INDEX by_stars ON notes (stars DESC, \"made at\"); +CREATE INDEX by_body ON notes (body); +" + ); +} + +#[test] +fn a_default_is_kept_in_the_type_the_author_wrote() { + let literal = Expr::Literal; + let minus = |literal| Expr::UnaryOp { + op: UnaryOp::Neg, + operand: Box::new(Expr::Literal(literal)), + }; + let now = Expr::FunctionCall { + name: "now".to_owned(), + args: Vec::new(), + }; + for (expr, value) in [ + (literal(Literal::Null), Value::Null), + (literal(Literal::Bool(true)), Value::Bool(true)), + (literal(Literal::Integer(3)), Value::Int64(3)), + (literal(Literal::Real(0.0)), Value::Float64(0.0)), + (literal(Literal::Text("it's".to_owned())), "it's".into()), + (minus(Literal::Integer(1)), Value::Int64(-1)), + (minus(Literal::Real(2.5)), Value::Float64(-2.5)), + ] { + assert_eq!(check::default(&expr), Some(ColumnDefault::Literal(value))); + } + assert_eq!(check::default(&now), Some(ColumnDefault::Now)); + // Anything that is not a literal or NOW() is no default this version keeps. + assert_eq!(check::default(&Expr::Parameter("p".to_owned())), None); + assert_eq!(check::default(&minus(Literal::Text("x".to_owned()))), None); +} + +/// A column as the parser hands over one that wrote no clause. +fn declared(name: &str, data_type: DataType) -> ast::ColumnDef { + ast::ColumnDef { + name: name.to_owned(), + data_type, + nullable: true, + primary_key: false, + unique: false, + default: None, + } +} + +/// A CREATE TABLE as the planner hands over one that wrote no table option. +fn create(name: &str, columns: Vec, unique: &[&[&str]]) -> CreateTablePlan { + CreateTablePlan { + name: name.to_owned(), + columns, + unique_constraints: unique + .iter() + .map(|rule| rule.iter().map(|column| (*column).to_owned()).collect()) + .collect(), + if_not_exists: false, + } +} + +#[test] +fn a_declared_table_gets_its_numbers_and_the_index_behind_each_rule() { + let key = |name: &str| ast::ColumnDef { + primary_key: true, + ..declared(name, DataType::Integer) + }; + let people = create( + "people", + vec![ + ast::ColumnDef { + nullable: false, + unique: true, + ..key("id") + }, + ast::ColumnDef { + unique: true, + ..declared("email", DataType::Text) + }, + ast::ColumnDef { + default: Some(Expr::Literal(Literal::Real(0.0))), + ..declared("score", DataType::Real) + }, + declared("born", DataType::Integer), + declared("doc", DataType::Json), + ], + &[&["email", "born"]], + ); + let mut catalog = Catalog { + tables: BTreeMap::new(), + }; + let table = check::new_table(&people, &catalog).expect("a plain table"); + let names = |table: &Table| -> Vec { + let indexes = table.meta.indexes.iter(); + indexes.map(|index| index.name.clone()).collect() + }; + assert_eq!(table.id, TableId(0)); + // The key column keeps the UNIQUE its author wrote and gets no second index for it. + assert_eq!( + names(&table), + ["__pk_id", "__unique_email", "__unique_email_born"] + ); + assert_eq!(table.index_ids, [0, 1, 2].map(IndexId)); + assert!(render(&table).starts_with( + "CREATE TABLE people (\n id INTEGER PRIMARY KEY NOT NULL UNIQUE,\n email TEXT UNIQUE,\n \ + score REAL DEFAULT 0.0,\n" + )); + catalog.tables.insert(table.name.clone(), table); + + // The next table and the next index count on from the ones the catalog holds. + let tags = create("tags", vec![key("id")], &[]); + let tags = check::new_table(&tags, &catalog).expect("a second table"); + assert_eq!( + (tags.id, &tags.index_ids[..]), + (TableId(1), &[IndexId(3)][..]) + ); + catalog.tables.insert(tags.name.clone(), tags); + let index = |name: &str, column: &str| CreateIndexPlan { + name: name.to_owned(), + table: "people".to_owned(), + columns: vec![(column.to_owned(), SortDirection::Desc)], + }; + let indexed = check::with_index(&index("by_born", "born"), &catalog).expect("an index"); + assert_eq!(names(&indexed)[3], "by_born"); + assert_eq!(indexed.index_ids, [0, 1, 2, 4].map(IndexId)); + catalog.tables.insert(indexed.name.clone(), indexed); + + // What is refused, and as what. + assert!(matches!( + check::with_index(&index("__unique_born", "born"), &catalog), + Err(Error::ReservedIndexName { prefix, .. }) if prefix == "__unique_" + )); + assert!(matches!( + check::with_index(&index("by_age", "age"), &catalog), + Err(Error::ColumnNotFound { table, column }) if table == "people" && column == "age" + )); + assert!(matches!( + check::with_index(&index("by_doc", "doc"), &catalog), + Err(Error::ColumnNotIndexable { column, column_type: ColumnType::Json, .. }) + if column == "doc" + )); + // A REAL may be indexed, and may not be a key or under a UNIQUE rule. + check::with_index(&index("by_score", "score"), &catalog).expect("an index over a REAL"); + for unique in [&[][..], &[&["id", "ratio"][..]][..]] { + let ratio = ast::ColumnDef { + unique: unique.is_empty(), + ..declared("ratio", DataType::Real) + }; + assert!(matches!( + check::new_table(&create("ratios", vec![key("id"), ratio], unique), &catalog), + Err(Error::ColumnNotIndexable { column, column_type: ColumnType::Real, .. }) + if column == "ratio" + )); + } + assert!(matches!( + check::with_index(&index("by_born", "email"), &catalog), + Err(Error::DuplicateIndex { table, index }) if table == "people" && index == "by_born" + )); + // Rules whose columns spell one name stay separate rules, each with its own index. + let pairs = create( + "pairs", + vec![ + key("id"), + declared("a", DataType::Integer), + declared("b", DataType::Integer), + declared("a_b", DataType::Integer), + ], + &[&["a_b"], &["a", "b"], &["a", "b"]], + ); + let pairs = check::new_table(&pairs, &catalog).expect("three rules"); + assert_eq!( + names(&pairs), + [ + "__pk_id", + "__unique_a_b", + "__unique_a_b_2", + "__unique_a_b_3" + ] + ); + assert_eq!(pairs.meta.indexes[1].columns.len(), 1); + assert_eq!(pairs.meta.indexes[2].columns.len(), 2); + let not_supported = |plan: &CreateTablePlan, clause: &str| { + matches!( + check::new_table(plan, &catalog), + Err(Error::PlanError(text)) if text.starts_with(clause) && text.contains("not supported") + ) + }; + let keyless = create("keyless", vec![declared("word", DataType::Text)], &[]); + assert!(not_supported(&keyless, "a table without a PRIMARY KEY")); +} diff --git a/v2/contextdb/src/sql/ast.rs b/v2/contextdb/src/sql/ast.rs new file mode 100644 index 000000000..fbc96b757 --- /dev/null +++ b/v2/contextdb/src/sql/ast.rs @@ -0,0 +1,162 @@ +//! A statement as it was written: what `parse` returns and `plan` reads. + +pub use crate::types::SortDirection; + +#[derive(Debug, Clone)] +pub enum Statement { + CreateTable(CreateTable), + CreateIndex(CreateIndex), + Insert(Insert), + Delete(Delete), + Update(Update), + Select(Select), + SetMemoryLimit(SetMemoryLimitValue), + ShowMemoryLimit, +} + +#[derive(Debug, Clone)] +pub enum SetMemoryLimitValue { + Bytes(usize), + None, +} + +#[derive(Debug, Clone)] +pub struct Select { + /// `SELECT *` is one column named `*`. + pub columns: Vec, + pub table: String, + pub where_clause: Option, + pub order_by: Vec, + pub limit: Option, +} + +#[derive(Debug, Clone)] +pub struct SelectColumn { + pub expr: Expr, + pub alias: Option, +} + +#[derive(Debug, Clone)] +pub struct OrderByItem { + pub expr: Expr, + pub direction: SortDirection, +} + +#[derive(Debug, Clone)] +pub enum Expr { + Column(ColumnRef), + Literal(Literal), + Parameter(String), + BinaryOp { + left: Box, + op: BinOp, + right: Box, + }, + UnaryOp { + op: UnaryOp, + operand: Box, + }, + /// A call by name. `a + b` arrives as the call `__add(a, b)`, and likewise `__sub`, `__mul` + /// and `__div`. The `*` of `COUNT(*)` is a column named `*`. + FunctionCall { + name: String, + args: Vec, + }, + IsNull { + expr: Box, + negated: bool, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ColumnRef { + pub table: Option, + pub column: String, +} + +#[derive(Debug, Clone)] +pub enum Literal { + Null, + Bool(bool), + Integer(i64), + Real(f64), + Text(String), + Vector(Vec), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BinOp { + Eq, + Neq, + Lt, + Lte, + Gt, + Gte, + And, + Or, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UnaryOp { + Not, + Neg, +} + +#[derive(Debug, Clone)] +pub struct Insert { + pub table: String, + /// Empty when the statement names no columns. + pub columns: Vec, + pub values: Vec>, +} + +#[derive(Debug, Clone)] +pub struct Delete { + pub table: String, + pub where_clause: Option, +} + +#[derive(Debug, Clone)] +pub struct Update { + pub table: String, + pub assignments: Vec<(String, Expr)>, + pub where_clause: Option, +} + +#[derive(Debug, Clone)] +pub struct CreateTable { + pub name: String, + pub columns: Vec, + /// The columns of each table-level `UNIQUE (...)`, in the order written. + pub unique_constraints: Vec>, + pub if_not_exists: bool, +} + +#[derive(Debug, Clone)] +pub struct ColumnDef { + pub name: String, + pub data_type: DataType, + pub nullable: bool, + pub primary_key: bool, + pub unique: bool, + pub default: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DataType { + Uuid, + Text, + Integer, + Real, + Boolean, + Timestamp, + Json, + Vector(u32), +} + +#[derive(Debug, Clone)] +pub struct CreateIndex { + pub name: String, + pub table: String, + pub columns: Vec<(String, SortDirection)>, +} diff --git a/v2/contextdb/src/sql/grammar.pest b/v2/contextdb/src/sql/grammar.pest new file mode 100644 index 000000000..909af2aef --- /dev/null +++ b/v2/contextdb/src/sql/grammar.pest @@ -0,0 +1,157 @@ +WHITESPACE = _{ SPACE | line_comment | block_comment } +SPACE = _{ " " | "\t" | "\r" | "\n" } +line_comment = _{ "--" ~ (!"\n" ~ ANY)* } +block_comment = _{ "/*" ~ (!"*/" ~ ANY)* ~ "*/" } + +statement = { SOI ~ stmt ~ ";"? ~ EOI } +stmt = _{ + create_table_stmt + | create_index_stmt + | insert_stmt + | delete_stmt + | update_stmt + | set_memory_limit + | show_memory_limit + | select_stmt + | later +} + +// A statement or clause that a later version of the library reads. It is recognised by its +// first words alone and the rest of the text is left unread, so that the statement is refused by +// that name and not as a syntax error. `later` stands only where nothing this version reads can +// begin with one of these words: after a whole statement or clause, and after an expression, +// where IN, LIKE, BETWEEN and the distance operator `<=>` would follow. DISTINCT and GRAPH_TABLE +// stand where a name could, so they are looked for first. +later = { later_name ~ ANY* } +later_name = @{ + "<=>" + | ( + ^"ALTER" | ^"DROP" | ^"WITH" | ^"BEGIN" | ^"COMMIT" | ^"ROLLBACK" | ^"PURGE" | ^"DISCARD" + | ^"DECLARE" | ^"DISTINCT" | ^"GRAPH_TABLE" + | ((^"LEFT" | ^"INNER" | ^"CROSS") ~ SPACE+)? ~ ^"JOIN" + | ^"GROUP" ~ SPACE+ ~ ^"BY" | ^"HAVING" | ^"UNION" | ^"OFFSET" + | ^"USE" ~ SPACE+ ~ (^"VECTOR" | ^"RANK") + | ^"ON" ~ SPACE+ ~ ^"CONFLICT" + | (^"NOT" ~ SPACE+)? ~ (^"IN" | ^"LIKE" | ^"BETWEEN") + | ^"FOREIGN" ~ SPACE+ ~ ^"KEY" | ^"REFERENCES" | ^"EXPIRES" | ^"IMMUTABLE" + | ^"RANK_POLICY" | ^"CONTEXT_ID" | ^"SCOPE_LABEL_READ" | ^"SCOPE_LABEL" | ^"ACL" + | ^"PARTITION_KEY" | ^"MAX_PARTITIONS" | ^"SEARCH_MODE" | ^"AUTO_INDEX_AT" | ^"SEARCH" + | ^"IVF" | ^"CONSOLIDATION" + | ^"RETAIN" | ^"HISTORY" | ^"SYNC" | ^"STATE_MACHINE" | ^"STATE" ~ SPACE+ ~ ^"MACHINE" + | ^"DAG" | ^"PROPAGATE" | ^"DELIVERY" ~ SPACE+ ~ ^"MANIFEST" + | ^"EDGE" ~ SPACE+ ~ ^"DISCARD" | ^"MAINTAIN" ~ SPACE+ ~ ^"COUNT" + ) ~ !ident_char +} + +set_memory_limit = { ^"SET" ~ ^"MEMORY_LIMIT" ~ "="? ~ ("'" ~ memory_limit_value ~ "'" | memory_limit_value) } +show_memory_limit = { ^"SHOW" ~ ^"MEMORY_LIMIT" } +memory_limit_value = { ^"none" | size_with_unit } +size_with_unit = @{ ASCII_DIGIT+ ~ ("G" | "M" | "K") } + +select_stmt = { + ^"SELECT" + ~ ( + &^"DISTINCT" ~ later + | select_list ~ ^"FROM" + ~ ( + &(^"GRAPH_TABLE" ~ "(") ~ later + | identifier ~ where_clause? ~ order_by_clause? ~ limit_clause? ~ later? + ) + ) +} +select_list = { star | select_item ~ ("," ~ select_item)* } +star = { "*" } +select_item = { expr ~ (^"AS" ~ identifier)? } +where_clause = { ^"WHERE" ~ expr } +order_by_clause = { ^"ORDER" ~ ^"BY" ~ order_item ~ ("," ~ order_item)* } +order_item = { expr ~ sort_dir? } +sort_dir = { ^"ASC" | ^"DESC" } +limit_clause = { ^"LIMIT" ~ integer } + +create_table_stmt = { + ^"CREATE" ~ ^"TABLE" ~ if_not_exists? ~ identifier + ~ "(" ~ table_element ~ ("," ~ table_element)* + // A later clause may follow a column, stand as an element of its own, or follow the list. + ~ (")" ~ later? | ","? ~ later) +} +if_not_exists = { ^"IF" ~ ^"NOT" ~ ^"EXISTS" } +table_element = _{ column_def | unique_table_constraint } +unique_table_constraint = { ^"UNIQUE" ~ "(" ~ identifier ~ ("," ~ identifier)* ~ ")" } +column_def = { identifier ~ data_type ~ column_constraint* } +data_type = { + vector_type + | ^"UUID" | ^"TEXT" | ^"INTEGER" | ^"INT" | ^"REAL" | ^"FLOAT" | ^"BOOLEAN" | ^"BOOL" + | ^"TIMESTAMP" | ^"JSON" +} +vector_type = { ^"VECTOR" ~ "(" ~ integer ~ ")" } +column_constraint = _{ not_null | nullable_marker | primary_key | unique | default_clause } +not_null = { ^"NOT" ~ ^"NULL" } +nullable_marker = { ^"NULL" } +primary_key = { ^"PRIMARY" ~ ^"KEY" } +unique = { ^"UNIQUE" } +default_clause = { ^"DEFAULT" ~ expr } + +create_index_stmt = { + ^"CREATE" ~ ^"INDEX" ~ identifier ~ ^"ON" ~ identifier + ~ "(" ~ indexed_column ~ ("," ~ indexed_column)* ~ ")" +} +indexed_column = { identifier ~ sort_dir? } + +insert_stmt = { + ^"INSERT" ~ ^"INTO" ~ identifier + ~ ("(" ~ identifier ~ ("," ~ identifier)* ~ ")")? + ~ ^"VALUES" ~ values_row ~ ("," ~ values_row)* + ~ later? +} +values_row = { "(" ~ expr ~ ("," ~ expr)* ~ ")" } +assignment = { identifier ~ "=" ~ expr } + +delete_stmt = { ^"DELETE" ~ ^"FROM" ~ identifier ~ where_clause? } +update_stmt = { ^"UPDATE" ~ identifier ~ ^"SET" ~ assignment ~ ("," ~ assignment)* ~ where_clause? } + +// One level of rule here is one level of the parser's recursion for every parenthesis, so the +// boolean operators share one flat rule and the arithmetic operators another, and the builder +// applies their precedence: NOT, then AND, then OR; `*` and `/` before `+` and `-`. +expr = { not_op* ~ additive_expr ~ comparison_suffix? ~ (bool_op ~ not_op* ~ additive_expr ~ comparison_suffix?)* } +bool_op = _{ and_op | or_op } +or_op = @{ ^"OR" ~ !ident_char } +and_op = @{ ^"AND" ~ !ident_char } +not_op = @{ ^"NOT" ~ !ident_char } + +comparison_suffix = _{ is_null_suffix | cmp_suffix | later } +cmp_suffix = { cmp_op ~ additive_expr } +cmp_op = { "=" | "!=" | "<>" | "<=" | ">=" | "<" | ">" } +is_null_suffix = { ^"IS" ~ not_op? ~ ^"NULL" } + +additive_expr = { unary_minus* ~ primary_expr ~ (math_op ~ unary_minus* ~ primary_expr)* } +math_op = { "+" | "-" | "*" | "/" } +unary_minus = { "-" } + +primary_expr = _{ + function_call + | parameter + | null_lit + | bool_lit + | float + | integer + | string + | vector_lit + | column_ref + | "(" ~ expr ~ ")" +} +vector_lit = { "[" ~ vector_component ~ ("," ~ vector_component)* ~ "]" } +vector_component = @{ "-"? ~ ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT+)? } +function_call = { identifier ~ "(" ~ (star | (expr ~ ("," ~ expr)*))? ~ ")" } +column_ref = { identifier ~ ("." ~ identifier)? } + +parameter = @{ "$" ~ ident } +null_lit = { ^"NULL" } +bool_lit = { ^"TRUE" | ^"FALSE" } +float = @{ ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT+ } +integer = @{ ASCII_DIGIT+ } +string = @{ "'" ~ ("''" | (!"'" ~ ANY))* ~ "'" } + +ident_char = _{ ASCII_ALPHANUMERIC | "_" } +ident = @{ (ASCII_ALPHA | "_") ~ ident_char* } +quoted_ident = @{ "\"" ~ ("\"\"" | (!"\"" ~ ANY))* ~ "\"" } +identifier = { quoted_ident | ident } diff --git a/v2/contextdb/src/sql/mod.rs b/v2/contextdb/src/sql/mod.rs new file mode 100644 index 000000000..91a5f3c7c --- /dev/null +++ b/v2/contextdb/src/sql/mod.rs @@ -0,0 +1,29 @@ +//! sql text: grammar, parser, planner. +//! +//! It never touches storage: it turns text into a statement and a statement into a plan, and +//! knows nothing of what any table holds. +//! +//! The fourth thing the parts meet at is defined here: [`Plan`], the planned statement that +//! `schema`, `write` and `query` each take. + +pub(crate) mod ast; +mod parser; +pub(crate) mod plan; +mod planner; + +pub(crate) use ast::{Expr, Statement}; +pub(crate) use plan::{Effect, PhysicalPlan as Plan}; + +use crate::types::Result; + +/// Reads one SQL statement. Refuses with `ParseError`, or with `PlanError` when the text begins +/// a statement or a clause that a later version of the library reads. Costs time in proportion +/// to the text. +pub(crate) fn parse(text: &str) -> Result { + parser::parse(text) +} + +/// Plans a parsed statement. Refuses with `PlanError`. Looks at no table and no data. +pub(crate) fn plan(statement: &Statement) -> Result { + planner::plan(statement) +} diff --git a/v2/contextdb/src/sql/parser/expr.rs b/v2/contextdb/src/sql/parser/expr.rs new file mode 100644 index 000000000..15249888c --- /dev/null +++ b/v2/contextdb/src/sql/parser/expr.rs @@ -0,0 +1,243 @@ +//! Expressions. The grammar reads the boolean operators as one flat run and the arithmetic +//! operators as another; the functions here apply their precedence. +//! +//! `expr`, `comparison_expr`, `additive_expr` and `primary_expr` call each other once for every +//! level of nesting, to the limit, on the caller's stack. So each is kept small: what is not on +//! that path is in a function of its own, and a list is gathered with a loop. + +use super::{MAX_EXPR_NESTING, Pair, Rule, depth_error, has, ident, number, string}; +use crate::sql::ast::*; +use crate::types::Result; + +pub(super) fn expr(pair: Pair<'_>) -> Result { + // `[NOT] comparison {AND | OR [NOT] comparison}`. AND binds tighter than OR: `all` gathers + // the comparisons joined by AND, `any` the runs between ORs. + let mut any = Vec::new(); + let mut all = Vec::new(); + let mut nots = 0; + let mut parts = pair.into_inner().peekable(); + while let Some(part) = parts.next() { + match part.as_rule() { + Rule::not_op => nots += 1, + Rule::and_op => {} + Rule::or_op => any.push(balanced(std::mem::take(&mut all), BinOp::And)), + _ => { + let suffix = + parts.next_if(|next| !matches!(next.as_rule(), Rule::and_op | Rule::or_op)); + let comparison = comparison_expr(part, suffix)?; + all.push( + (0..std::mem::take(&mut nots)).fold(comparison, |operand, _| not(operand)), + ); + } + } + } + any.push(balanced(all, BinOp::And)); + let built = balanced(any, BinOp::Or); + let depth = depth_of(&built); + if depth > MAX_EXPR_NESTING { + return Err(depth_error(depth)); + } + Ok(built) +} + +/// Every expression directly inside `pair`, in order. +pub(super) fn exprs(pair: Pair<'_>) -> Result> { + let mut exprs = Vec::new(); + for part in pair.into_inner() { + if part.as_rule() == Rule::expr { + exprs.push(expr(part)?); + } + } + Ok(exprs) +} + +/// How many expressions deep the tree goes below its root. +fn depth_of(expr: &Expr) -> usize { + let below = match expr { + Expr::BinaryOp { left, right, .. } => depth_of(left).max(depth_of(right)), + Expr::UnaryOp { operand, .. } => depth_of(operand), + Expr::FunctionCall { args, .. } => args.iter().map(depth_of).max().unwrap_or(0), + Expr::IsNull { expr, .. } => depth_of(expr), + Expr::Column(_) | Expr::Literal(_) | Expr::Parameter(_) => return 0, + }; + below + 1 +} + +/// Joins the operands of a run of ANDs or ORs into a tree as shallow as it can be, keeping +/// their order, so a long run does not count against the nesting limit. +fn balanced(operands: Vec, op: BinOp) -> Expr { + fn build(operands: &mut std::vec::IntoIter, len: usize, op: BinOp) -> Expr { + if len == 1 { + return operands.next().expect("an operand"); + } + let left = build(operands, len / 2, op); + let right = build(operands, len - len / 2, op); + Expr::BinaryOp { + left: Box::new(left), + op, + right: Box::new(right), + } + } + let len = operands.len(); + build(&mut operands.into_iter(), len, op) +} + +fn not(operand: Expr) -> Expr { + Expr::UnaryOp { + op: UnaryOp::Not, + operand: Box::new(operand), + } +} + +/// One comparison: its left side and what the grammar read after it, if anything. +fn comparison_expr(left: Pair<'_>, suffix: Option>) -> Result { + let left = Box::new(additive_expr(left)?); + let Some(suffix) = suffix else { + return Ok(*left); + }; + if suffix.as_rule() == Rule::is_null_suffix { + return Ok(Expr::IsNull { + expr: left, + negated: has(&suffix, Rule::not_op), + }); + } + let mut parts = suffix.into_inner(); + let op = match parts.next().expect("an operator").as_str() { + "=" => BinOp::Eq, + "!=" | "<>" => BinOp::Neq, + "<" => BinOp::Lt, + "<=" => BinOp::Lte, + ">" => BinOp::Gt, + _ => BinOp::Gte, + }; + Ok(Expr::BinaryOp { + left, + op, + right: Box::new(additive_expr(parts.next().expect("an operand"))?), + }) +} + +/// `left right` as a call of `__add`, `__sub`, `__mul` or `__div`. A chain of operators +/// builds a tree one deeper per operator, so its depth is checked as it grows. +fn arithmetic(op: &str, left: Expr, right: Expr) -> Result { + let depth = depth_of(&left).max(depth_of(&right)) + 1; + if depth > MAX_EXPR_NESTING { + return Err(depth_error(depth)); + } + let name = match op { + "+" => "__add", + "-" => "__sub", + "*" => "__mul", + _ => "__div", + }; + Ok(Expr::FunctionCall { + name: name.to_owned(), + args: vec![left, right], + }) +} + +/// `[-] primary {+ | - | * | / [-] primary}`, read left to right with `*` and `/` binding +/// tighter. +fn additive_expr(pair: Pair<'_>) -> Result { + // `sum` is everything before the last `+` or `-`, with that operator; `product` is the run + // of `*` and `/` being read, with the operator that waits for its right side. + let mut sum: Option<(Expr, &str)> = None; + let mut product: Option<(Expr, &str)> = None; + let mut minuses = 0; + for part in pair.into_inner() { + match part.as_rule() { + Rule::unary_minus => minuses += 1, + Rule::math_op => { + let (operand, _) = product.take().expect("an operand before an operator"); + let op = part.as_str(); + if op == "*" || op == "/" { + product = Some((operand, op)); + } else { + let total = match sum.take() { + Some((left, plus_or_minus)) => arithmetic(plus_or_minus, left, operand)?, + None => operand, + }; + sum = Some((total, op)); + } + } + _ => { + let mut operand = primary_expr(part)?; + for _ in 0..std::mem::take(&mut minuses) { + operand = Expr::UnaryOp { + op: UnaryOp::Neg, + operand: Box::new(operand), + }; + } + product = Some(match product.take() { + Some((left, times_or_over)) => (arithmetic(times_or_over, left, operand)?, ""), + None => (operand, ""), + }); + } + } + } + let (operand, _) = product.expect("an operand"); + match sum { + Some((left, plus_or_minus)) => arithmetic(plus_or_minus, left, operand), + None => Ok(operand), + } +} + +/// The `*` of `SELECT *` and of `COUNT(*)`: a column of that name. +pub(super) fn star() -> Expr { + Expr::Column(ColumnRef { + table: None, + column: "*".to_owned(), + }) +} + +fn primary_expr(primary: Pair<'_>) -> Result { + match primary.as_rule() { + // A parenthesised expression. + Rule::expr => expr(primary), + Rule::function_call => function_call(primary), + Rule::column_ref => Ok(column_ref(primary)), + _ => literal(&primary), + } +} + +fn literal(pair: &Pair<'_>) -> Result { + Ok(match pair.as_rule() { + Rule::parameter => Expr::Parameter(pair.as_str()[1..].to_owned()), + Rule::null_lit => Expr::Literal(Literal::Null), + Rule::bool_lit => Expr::Literal(Literal::Bool(pair.as_str().eq_ignore_ascii_case("true"))), + Rule::float => Expr::Literal(Literal::Real(number(pair, "the number")?)), + Rule::integer => Expr::Literal(Literal::Integer(number(pair, "the whole number")?)), + Rule::string => Expr::Literal(Literal::Text(string(pair))), + // The one literal left: a vector. + _ => { + let components = pair + .clone() + .into_inner() + .map(|component| number(&component, "the vector component")) + .collect::>()?; + Expr::Literal(Literal::Vector(components)) + } + }) +} + +fn column_ref(pair: Pair<'_>) -> Expr { + let mut names: Vec = pair.into_inner().map(|name| ident(&name)).collect(); + let column = names.pop().expect("a column name"); + Expr::Column(ColumnRef { + table: names.pop(), + column, + }) +} + +fn function_call(pair: Pair<'_>) -> Result { + let mut parts = pair.into_inner(); + let name = ident(&parts.next().expect("a function name")); + let mut args = Vec::new(); + for arg in parts { + args.push(match arg.as_rule() { + Rule::star => star(), + _ => expr(arg)?, + }); + } + Ok(Expr::FunctionCall { name, args }) +} diff --git a/v2/contextdb/src/sql/parser/mod.rs b/v2/contextdb/src/sql/parser/mod.rs new file mode 100644 index 000000000..3a06ff0a3 --- /dev/null +++ b/v2/contextdb/src/sql/parser/mod.rs @@ -0,0 +1,463 @@ +//! Reads SQL text into a [`Statement`]. The grammar is `grammar.pest`; the functions here and in +//! the files beside this one turn what the grammar matched into the statement types. +//! +//! A builder trusts the grammar for the shape of what it is handed (which parts are present and +//! in what order) and checks only what a grammar cannot say: a number's range, a clause written +//! twice, two clauses that contradict each other. + +mod expr; + +use std::str::FromStr; + +use pest::Parser; +use pest::error::{InputLocation, LineColLocation}; + +use super::ast::*; +use crate::types::{Error, Result}; + +#[derive(pest_derive::Parser)] +#[grammar = "sql/grammar.pest"] +struct SqlParser; + +type Pair<'a> = pest::iterators::Pair<'a, Rule>; + +/// The deepest an expression may nest, in the text and in the tree built from it. It bounds the +/// parser's own recursion and the recursion of everything that later walks an expression. +const MAX_EXPR_NESTING: usize = 128; + +pub(super) fn parse(text: &str) -> Result { + check_source_nesting(text)?; + let read = + SqlParser::parse(Rule::statement, text).map_err(|error| syntax_error(text, &error))?; + if let Some(later) = read + .clone() + .flatten() + .find(|part| part.as_rule() == Rule::later_name) + { + let words: Vec<&str> = later.as_str().split_whitespace().collect(); + return Err(Error::PlanError(format!( + "{} is not supported in this version of the library", + words.join(" ").to_ascii_uppercase() + ))); + } + let pair = read + .flatten() + .nth(1) + .expect("the grammar's statement rule holds one statement"); + match pair.as_rule() { + Rule::create_table_stmt => create_table(pair).map(Statement::CreateTable), + Rule::create_index_stmt => { + let [name, table] = two_idents(pair.clone()); + Ok(Statement::CreateIndex(CreateIndex { + name, + table, + columns: pair + .into_inner() + .filter(|part| part.as_rule() == Rule::indexed_column) + .map(|column| { + let mut parts = column.into_inner(); + let name = ident(&parts.next().expect("a column")); + (name, direction(parts.next())) + }) + .collect(), + })) + } + Rule::insert_stmt => insert(pair).map(Statement::Insert), + Rule::delete_stmt => Ok(Statement::Delete(Delete { + table: first_ident(pair.clone()), + where_clause: where_clause(pair)?, + })), + Rule::update_stmt => Ok(Statement::Update(Update { + table: first_ident(pair.clone()), + assignments: assignments(pair.clone())?, + where_clause: where_clause(pair)?, + })), + Rule::select_stmt => select(pair).map(Statement::Select), + Rule::set_memory_limit => memory_limit(pair).map(Statement::SetMemoryLimit), + // The one statement left: SHOW MEMORY_LIMIT. + _ => Ok(Statement::ShowMemoryLimit), + } +} + +/// The refusal for text the grammar does not read: where it stopped and the word it stopped at. +fn syntax_error(text: &str, error: &pest::error::Error) -> Error { + let (LineColLocation::Pos((line, column)) | LineColLocation::Span((line, column), _)) = + error.line_col; + let (InputLocation::Pos(at) | InputLocation::Span((at, _))) = error.location; + let word: String = text[at..] + .trim_start() + .chars() + .take_while(|ch| !ch.is_whitespace()) + .take(64) + .collect(); + if word.is_empty() { + refuse(format!( + "syntax error at line {line}, column {column}: the statement ends too soon" + )) + } else { + refuse(format!( + "syntax error at line {line}, column {column}: unexpected '{word}'" + )) + } +} + +fn depth_error(depth: usize) -> Error { + refuse(format!( + "expression nesting of {depth} is deeper than the limit of {MAX_EXPR_NESTING}" + )) +} + +/// The words and marks of a statement, without its comments. A quoted text or name comes as its +/// opening quote alone, so nothing inside quotes is ever taken for a keyword or a parenthesis. +struct Tokens<'a> { + text: &'a str, + at: usize, +} + +impl<'a> Iterator for Tokens<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option<&'a str> { + loop { + let rest = &self.text[self.at..]; + let first = rest.chars().next()?; + let len = if first.is_whitespace() { + first.len_utf8() + } else if rest.starts_with("--") { + rest.find('\n').unwrap_or(rest.len()) + } else if rest.starts_with("/*") { + rest.find("*/").map_or(rest.len(), |end| end + 2) + } else { + break; + }; + self.at += len; + } + let rest = &self.text[self.at..]; + let first = rest.chars().next()?; + let (token, consumed) = if first == '\'' || first == '"' { + // A doubled quote inside is the quote itself, so the text ends at the first quote + // that is not followed by another. + let mut end = 1; + loop { + match rest[end..].find(first) { + None => break (&rest[..1], rest.len()), + Some(quote) if rest[end + quote + 1..].starts_with(first) => end += quote + 2, + Some(quote) => break (&rest[..1], end + quote + 1), + } + } + } else if first.is_ascii_alphanumeric() || first == '_' { + let end = rest + .find(|ch: char| !ch.is_ascii_alphanumeric() && ch != '_') + .unwrap_or(rest.len()); + (&rest[..end], end) + } else { + (&rest[..first.len_utf8()], first.len_utf8()) + }; + self.at += consumed; + Some(token) + } +} + +/// Refuses text whose expressions nest deeper than the limit, before the grammar recurses into +/// it. A parenthesis, a NOT and a unary minus each count one level. +fn check_source_nesting(text: &str) -> Result<()> { + // The depth outside each open parenthesis, innermost last. + let mut frames = vec![0usize]; + // NOT and unary minus read since the last operand. + let mut prefixes = 0; + // Whether a minus read now would be unary. + let mut operand_next = true; + for token in (Tokens { text, at: 0 }) { + match token { + "(" => { + frames.push(frames[frames.len() - 1] + prefixes + 1); + prefixes = 0; + operand_next = true; + } + ")" => { + if frames.len() > 1 { + frames.pop(); + } + prefixes = 0; + operand_next = false; + } + "-" if operand_next => prefixes += 1, + _ if token.eq_ignore_ascii_case("NOT") => { + prefixes += 1; + operand_next = true; + } + _ => { + prefixes = 0; + operand_next = matches!(token, "=" | "<" | ">" | "+" | "-" | "*" | "/" | ",") + || ["AND", "OR", "WHERE", "SELECT", "BY"] + .iter() + .any(|word| token.eq_ignore_ascii_case(word)); + } + } + let depth = frames[frames.len() - 1] + prefixes; + if depth > MAX_EXPR_NESTING { + return Err(depth_error(depth)); + } + } + Ok(()) +} + +fn refuse(message: impl Into) -> Error { + Error::ParseError(message.into()) +} + +/// A name as written, or what is between the double quotes of a quoted one. +fn ident(pair: &Pair<'_>) -> String { + let raw = pair.as_str(); + match raw.strip_prefix('"').and_then(|raw| raw.strip_suffix('"')) { + Some(quoted) => quoted.replace("\"\"", "\""), + None => raw.to_owned(), + } +} + +/// What is between the single quotes of a quoted text. +fn string(pair: &Pair<'_>) -> String { + let raw = pair.as_str(); + raw[1..raw.len() - 1].replace("''", "'") +} + +/// Every name directly inside `pair`, in order. +fn idents(pair: Pair<'_>) -> Vec { + pair.into_inner() + .filter(|part| part.as_rule() == Rule::identifier) + .map(|part| ident(&part)) + .collect() +} + +fn first_ident(pair: Pair<'_>) -> String { + idents(pair).swap_remove(0) +} + +fn two_idents(pair: Pair<'_>) -> [String; 2] { + let mut names = idents(pair).into_iter(); + [(); 2].map(|()| names.next().expect("the grammar reads two names here")) +} + +/// The first part of `pair` that the grammar read with `rule`. +fn child<'a>(pair: Pair<'a>, rule: Rule) -> Option> { + pair.into_inner().find(|part| part.as_rule() == rule) +} + +fn has(pair: &Pair<'_>, rule: Rule) -> bool { + child(pair.clone(), rule).is_some() +} + +/// A number the grammar read as digits, refused when it does not fit the type it is for. +fn number(pair: &Pair<'_>, what: &str) -> Result { + pair.as_str() + .parse() + .map_err(|_| refuse(format!("{what} {} is out of range", pair.as_str()))) +} + +fn insert(pair: Pair<'_>) -> Result { + let mut values = Vec::new(); + for row in pair.clone().into_inner() { + if row.as_rule() == Rule::values_row { + values.push(expr::exprs(row)?); + } + } + let mut names = idents(pair); + let columns = names.split_off(1); + Ok(Insert { + table: names.swap_remove(0), + columns, + values, + }) +} + +/// Every `column = value` directly inside `pair`. +fn assignments(pair: Pair<'_>) -> Result> { + pair.into_inner() + .filter(|part| part.as_rule() == Rule::assignment) + .map(|assignment| { + let mut parts = assignment.into_inner(); + let column = ident(&parts.next().expect("a column")); + Ok((column, expr::expr(parts.next().expect("a value"))?)) + }) + .collect() +} + +fn where_clause(pair: Pair<'_>) -> Result> { + child(pair, Rule::where_clause) + .map(|clause| expr::expr(clause.into_inner().next().expect("a condition"))) + .transpose() +} + +/// ASC when nothing is written. +fn direction(written: Option>) -> SortDirection { + match written { + Some(written) if written.as_str().eq_ignore_ascii_case("DESC") => SortDirection::Desc, + _ => SortDirection::Asc, + } +} + +fn select(pair: Pair<'_>) -> Result