├── native_db_macro ├── .gitignore ├── README.md ├── src │ ├── struct_name.rs │ ├── lib.rs │ └── native_db.rs └── Cargo.toml ├── tests ├── custom_type │ ├── mod.rs │ └── custom.rs ├── metadata │ ├── mod.rs │ └── current_version.rs ├── check_type │ ├── mod.rs │ └── struct_custom.rs ├── primary_drain │ ├── mod.rs │ ├── only_primary_key.rs │ └── with_secondary_keys.rs ├── data │ ├── db_0_5_x │ ├── db_0_6_0 │ ├── db_0_7_1 │ └── db_0_8-pre-0 ├── migrate │ ├── mod.rs │ ├── only_primary_key.rs │ └── with_other_model.rs ├── modules.rs ├── macro_def │ ├── mod.rs │ ├── primary_key_attribute.rs │ ├── primary_key.rs │ ├── export_keys_attribute.rs │ ├── secondary_key_mix.rs │ └── secondary_key.rs ├── query │ ├── mod.rs │ ├── insert_len_pk.rs │ ├── insert_len_sk.rs │ ├── insert_remove_pk.rs │ ├── upsert_get_pk.rs │ ├── insert_get_pk.rs │ ├── insert_update_pk.rs │ ├── insert_update_sk.rs │ ├── auto_update_pk.rs │ ├── insert_get_sk.rs │ └── auto_update_sk.rs ├── check_integrity.rs ├── snapshot.rs ├── watch_tokio.rs ├── compact.rs ├── native_model.rs ├── deserialization_error.rs ├── test_upgrade_error.rs ├── simple_multithreads.rs ├── convert_all.rs ├── util.rs ├── upgrade_error.rs ├── watch │ └── watch_optional.rs └── transaction.rs ├── examples ├── major_upgrade │ ├── src │ │ ├── models │ │ │ ├── mod.rs │ │ │ ├── v08x.rs │ │ │ └── current_version.rs │ │ ├── lib.rs │ │ ├── main_old.rs │ │ └── new_main.rs │ ├── Cargo.toml │ └── tests │ │ ├── test_main_old.rs │ │ └── test_main.rs └── upgrade.rs ├── src ├── db_type │ ├── result.rs │ ├── key │ │ ├── key_value.rs │ │ └── mod.rs │ ├── mod.rs │ ├── to_input.rs │ ├── output.rs │ ├── input.rs │ ├── upgrade_required_error.rs │ └── error.rs ├── transaction │ ├── internal │ │ ├── mod.rs │ │ ├── r_transaction.rs │ │ └── private_readable_transaction.rs │ ├── query │ │ ├── mod.rs │ │ ├── drain.rs │ │ ├── len.rs │ │ └── scan │ │ │ └── mod.rs │ ├── mod.rs │ └── r_transaction.rs ├── metadata │ ├── current_version.rs │ ├── mod.rs │ ├── metadata.rs │ └── table.rs ├── stats.rs ├── serialization.rs ├── watch │ ├── request.rs │ ├── batch.rs │ ├── query │ │ ├── mod.rs │ │ ├── get.rs │ │ └── internal.rs │ ├── event.rs │ ├── mod.rs │ └── filter.rs ├── model.rs ├── snapshot.rs ├── table_definition.rs ├── upgrade.rs └── database_instance.rs ├── benches └── results │ ├── get_random.png │ ├── scan_random.png │ ├── insert_random.png │ └── remove_random.png ├── Cross.toml ├── .github └── workflows │ ├── conventional_commits.yml │ ├── fmt_check.yml │ ├── clippy_check.yml │ ├── pr_wait.yml │ ├── build_test_windows.yml │ ├── build_test_macos.yml │ ├── build_test_ios.yml │ ├── build_test_linux.yml │ ├── release.yml │ └── build_test_android.yml ├── .gitignore ├── .all-contributorsrc ├── cargo_publish.sh ├── LICENSE ├── renovate.json ├── Cargo.toml ├── tables.toml ├── .releaserc ├── version_update.sh ├── justfile └── README.md /native_db_macro/.gitignore: -------------------------------------------------------------------------------- 1 | target/ -------------------------------------------------------------------------------- /tests/custom_type/mod.rs: -------------------------------------------------------------------------------- 1 | mod custom; 2 | -------------------------------------------------------------------------------- /tests/metadata/mod.rs: -------------------------------------------------------------------------------- 1 | mod current_version; 2 | -------------------------------------------------------------------------------- /native_db_macro/README.md: -------------------------------------------------------------------------------- 1 | A procedural macro for native_db -------------------------------------------------------------------------------- /tests/check_type/mod.rs: -------------------------------------------------------------------------------- 1 | mod all; 2 | mod struct_custom; 3 | mod struct_simple; 4 | -------------------------------------------------------------------------------- /tests/primary_drain/mod.rs: -------------------------------------------------------------------------------- 1 | mod only_primary_key; 2 | mod with_secondary_keys; 3 | -------------------------------------------------------------------------------- /examples/major_upgrade/src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod current_version; 2 | pub mod v08x; 3 | -------------------------------------------------------------------------------- /examples/major_upgrade/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod main_old; 2 | pub mod models; 3 | pub mod new_main; 4 | -------------------------------------------------------------------------------- /tests/data/db_0_5_x: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincent-herlemont/native_db/HEAD/tests/data/db_0_5_x -------------------------------------------------------------------------------- /tests/data/db_0_6_0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincent-herlemont/native_db/HEAD/tests/data/db_0_6_0 -------------------------------------------------------------------------------- /tests/data/db_0_7_1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincent-herlemont/native_db/HEAD/tests/data/db_0_7_1 -------------------------------------------------------------------------------- /src/db_type/result.rs: -------------------------------------------------------------------------------- 1 | use super::Error; 2 | 3 | pub type Result = std::result::Result; 4 | -------------------------------------------------------------------------------- /tests/data/db_0_8-pre-0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincent-herlemont/native_db/HEAD/tests/data/db_0_8-pre-0 -------------------------------------------------------------------------------- /benches/results/get_random.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincent-herlemont/native_db/HEAD/benches/results/get_random.png -------------------------------------------------------------------------------- /benches/results/scan_random.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincent-herlemont/native_db/HEAD/benches/results/scan_random.png -------------------------------------------------------------------------------- /src/transaction/internal/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod private_readable_transaction; 2 | pub mod r_transaction; 3 | pub mod rw_transaction; 4 | -------------------------------------------------------------------------------- /benches/results/insert_random.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincent-herlemont/native_db/HEAD/benches/results/insert_random.png -------------------------------------------------------------------------------- /benches/results/remove_random.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vincent-herlemont/native_db/HEAD/benches/results/remove_random.png -------------------------------------------------------------------------------- /tests/migrate/mod.rs: -------------------------------------------------------------------------------- 1 | mod only_primary_key; 2 | mod with_multiple_versions; 3 | mod with_other_model; 4 | mod with_secondary_keys; 5 | -------------------------------------------------------------------------------- /src/metadata/current_version.rs: -------------------------------------------------------------------------------- 1 | pub const CURRENT_VERSION: &str = "0.8.1"; 2 | pub const CURRENT_NATIVE_MODEL_VERSION: &str = "0.4.19"; 3 | -------------------------------------------------------------------------------- /src/transaction/query/mod.rs: -------------------------------------------------------------------------------- 1 | mod drain; 2 | mod get; 3 | mod len; 4 | mod scan; 5 | 6 | pub use drain::*; 7 | pub use get::*; 8 | pub use len::*; 9 | pub use scan::*; 10 | -------------------------------------------------------------------------------- /tests/modules.rs: -------------------------------------------------------------------------------- 1 | mod check_type; 2 | mod custom_type; 3 | mod macro_def; 4 | 5 | mod metadata; 6 | mod migrate; 7 | mod primary_drain; 8 | mod query; 9 | mod watch; 10 | -------------------------------------------------------------------------------- /src/db_type/key/key_value.rs: -------------------------------------------------------------------------------- 1 | use crate::db_type::Key; 2 | 3 | #[derive(Debug, Clone, PartialEq, Eq)] 4 | pub enum KeyEntry { 5 | Default(Key), 6 | Optional(Option), 7 | } 8 | -------------------------------------------------------------------------------- /tests/macro_def/mod.rs: -------------------------------------------------------------------------------- 1 | mod export_keys_attribute; 2 | mod primary_key; 3 | mod primary_key_attribute; 4 | mod secondary_key; 5 | mod secondary_key_attribute; 6 | mod secondary_key_mix; 7 | -------------------------------------------------------------------------------- /src/db_type/key/mod.rs: -------------------------------------------------------------------------------- 1 | #[allow(clippy::module_inception)] 2 | mod key; 3 | mod key_definition; 4 | mod key_value; 5 | 6 | pub use key::*; 7 | 8 | pub use key_definition::*; 9 | pub use key_value::*; 10 | -------------------------------------------------------------------------------- /src/metadata/mod.rs: -------------------------------------------------------------------------------- 1 | mod current_version; 2 | #[allow(clippy::module_inception)] 3 | mod metadata; 4 | mod table; 5 | 6 | pub(crate) use current_version::*; 7 | pub use metadata::*; 8 | pub(crate) use table::*; 9 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [target.i686-linux-android] 2 | image = "ghcr.io/cross-rs/i686-linux-android:main" 3 | 4 | [target.aarch64-linux-android] 5 | image = "ghcr.io/cross-rs/aarch64-linux-android:main" 6 | 7 | [target.x86_64-linux-android] 8 | image = "ghcr.io/cross-rs/x86_64-linux-android:main" -------------------------------------------------------------------------------- /src/stats.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub struct Stats { 3 | pub primary_tables: Vec, 4 | pub secondary_tables: Vec, 5 | } 6 | 7 | #[derive(Debug)] 8 | pub struct StatsTable { 9 | pub name: String, 10 | pub n_entries: Option, 11 | } 12 | -------------------------------------------------------------------------------- /src/transaction/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod internal; 2 | 3 | /// All database interactions. 4 | pub mod query; 5 | 6 | mod r_transaction; 7 | 8 | mod rw_transaction; 9 | 10 | /// Read-only transaction. 11 | pub use r_transaction::*; 12 | /// Read-write transaction. 13 | pub use rw_transaction::*; 14 | -------------------------------------------------------------------------------- /native_db_macro/src/struct_name.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Ident; 2 | 3 | #[derive(Clone, Debug)] 4 | pub(crate) struct StructName(Ident); 5 | 6 | impl StructName { 7 | pub(crate) fn ident(&self) -> &Ident { 8 | &self.0 9 | } 10 | pub(crate) fn new(ident: Ident) -> Self { 11 | Self(ident) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/db_type/mod.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | mod input; 3 | mod key; 4 | mod output; 5 | mod result; 6 | mod to_input; 7 | mod upgrade_required_error; 8 | 9 | pub use error::*; 10 | pub use input::*; 11 | pub use key::*; 12 | pub(crate) use output::*; 13 | pub use result::*; 14 | pub use to_input::*; 15 | pub use upgrade_required_error::*; 16 | -------------------------------------------------------------------------------- /tests/query/mod.rs: -------------------------------------------------------------------------------- 1 | // Insert 2 | mod insert_get_pk; 3 | mod insert_get_sk; 4 | mod insert_len_pk; 5 | mod insert_len_sk; 6 | mod insert_remove_pk; 7 | mod insert_remove_sk; 8 | mod insert_update_pk; 9 | mod insert_update_sk; 10 | 11 | // Upsert 12 | mod upsert_get_pk; 13 | mod upsert_get_sk; 14 | 15 | // Auto Update 16 | mod auto_update_pk; 17 | mod auto_update_sk; 18 | -------------------------------------------------------------------------------- /.github/workflows/conventional_commits.yml: -------------------------------------------------------------------------------- 1 | name: Conventional Commits 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | build: 9 | name: Conventional Commits 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 13 | - uses: webiny/action-conventional-commits@v1.3.0 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | 3 | # TODO: remove it used by semantic-release/exec 4 | node_modules/ 5 | package-lock.json 6 | package.json 7 | *_expanded.rs 8 | 9 | # Related to [Why do binaries have Cargo.lock in version control, but not libraries?](https://doc.rust-lang.org/cargo/faq.html#why-do-binaries-have-cargolock-in-version-control-but-not-libraries) 10 | Cargo.lock 11 | tests/data/db_x_x_x 12 | 13 | .DS_Store 14 | # IDE specific 15 | .cursor/ 16 | -------------------------------------------------------------------------------- /src/serialization.rs: -------------------------------------------------------------------------------- 1 | pub fn bincode_encode_to_vec(value: &T) -> crate::db_type::Result> 2 | where 3 | T: serde::Serialize + native_model::Model, 4 | { 5 | native_model::encode(value).map_err(|e| e.into()) 6 | } 7 | 8 | pub fn bincode_decode_from_slice(slice: &[u8]) -> crate::db_type::Result<(T, usize)> 9 | where 10 | T: serde::de::DeserializeOwned + native_model::Model, 11 | { 12 | let (data, _) = native_model::decode(slice.to_vec())?; 13 | Ok((data, 0)) 14 | } 15 | -------------------------------------------------------------------------------- /native_db_macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "native_db_macro" 3 | version = "0.8.1" 4 | authors = ["Vincent Herlemont "] 5 | edition = "2018" 6 | description = "A procedural macro for native_db" 7 | license = "MIT" 8 | repository = "https://github.com/vincent-herlemont/native_db" 9 | readme = "README.md" 10 | 11 | [lib] 12 | path = "src/lib.rs" 13 | proc-macro = true 14 | 15 | [dependencies] 16 | syn = { version = "2.0", features = ["full"] } 17 | quote = "1.0" 18 | proc-macro2 = "1.0" 19 | 20 | [features] 21 | default = [] -------------------------------------------------------------------------------- /examples/major_upgrade/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "major_upgrade" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [workspace] 7 | 8 | [dependencies] 9 | # Current version (from local path) 10 | native_model_current = { package = "native_model", version = "0.6.2" } 11 | native_db_current = { package = "native_db", path = "../.." } 12 | 13 | # Previous version (from external path) 14 | native_model_v0_4_x = { package = "native_model", version = "0.4.20" } 15 | native_db_v0_8_x = { package = "native_db", version = "0.8.2" } 16 | 17 | # Common dependencies 18 | serde = { version = "1.0", features = ["derive"] } 19 | -------------------------------------------------------------------------------- /tests/macro_def/primary_key_attribute.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] 6 | #[native_model(id = 1, version = 1)] 7 | #[native_db] 8 | struct Item { 9 | #[primary_key] 10 | id: u32, 11 | name: String, 12 | } 13 | // TODO: Test for other type enum tuple etc ... 14 | 15 | #[test] 16 | fn test_insert_my_item() { 17 | let item = Item { 18 | id: 1, 19 | name: "test".to_string(), 20 | }; 21 | 22 | let key = item.native_db_primary_key(); 23 | assert_eq!(key, 1_u32.to_key()); 24 | } 25 | -------------------------------------------------------------------------------- /examples/major_upgrade/src/models/v08x.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | // Import v0.8.1 version as native_db for macro expansion 4 | use native_db_v0_8_x as native_db; 5 | use native_db_v0_8_x::ToKey; 6 | 7 | // Import native_model macro version matched with the v0.8.1 version native_db for macro expansion. 8 | use native_model_v0_4_x as native_model; 9 | use native_model_v0_4_x::{native_model, Model}; 10 | 11 | // Model for v0.8.x 12 | #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] 13 | #[native_model(id = 1, version = 1)] 14 | #[native_db_v0_8_x::native_db] 15 | pub struct V08xModel { 16 | #[primary_key] 17 | pub id: u32, 18 | pub name: String, 19 | } 20 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "commitType": "docs", 8 | "commitConvention": "angular", 9 | "contributors": [ 10 | { 11 | "login": "elliot14A", 12 | "name": "Akshith Madhur", 13 | "avatar_url": "https://avatars.githubusercontent.com/u/84667163?v=4", 14 | "profile": "https://github.com/elliot14A", 15 | "contributions": [ 16 | "code" 17 | ] 18 | } 19 | ], 20 | "contributorsPerLine": 7, 21 | "skipCi": true, 22 | "repoType": "github", 23 | "repoHost": "https://github.com", 24 | "projectName": "native_db", 25 | "projectOwner": "vincent-herlemont" 26 | } 27 | -------------------------------------------------------------------------------- /native_db_macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | mod keys; 4 | mod model_attributes; 5 | mod model_native_db; 6 | mod native_db; 7 | mod struct_name; 8 | 9 | use proc_macro::TokenStream; 10 | 11 | use native_db::native_db as native_db_impl; 12 | 13 | #[proc_macro_attribute] 14 | pub fn native_db(args: TokenStream, input: TokenStream) -> TokenStream { 15 | native_db_impl(args, input) 16 | } 17 | 18 | #[proc_macro_derive(KeyAttributes, attributes(primary_key, secondary_key))] 19 | pub fn key_attributes(_input: TokenStream) -> TokenStream { 20 | let gen = quote::quote! {}; 21 | gen.into() 22 | } 23 | 24 | trait ToTokenStream { 25 | fn new_to_token_stream(&self) -> proc_macro2::TokenStream; 26 | } 27 | -------------------------------------------------------------------------------- /tests/macro_def/primary_key.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] 6 | #[native_model(id = 1, version = 1)] 7 | #[native_db(primary_key(compute_primary_key -> String))] 8 | struct Item { 9 | id: u32, 10 | name: String, 11 | } 12 | 13 | impl Item { 14 | pub fn compute_primary_key(&self) -> String { 15 | format!("{}-{}", self.id, self.name) 16 | } 17 | } 18 | 19 | #[test] 20 | fn test_insert_my_item() { 21 | let item = Item { 22 | id: 1, 23 | name: "test".to_string(), 24 | }; 25 | 26 | let key = item.native_db_primary_key(); 27 | assert_eq!(key, "1-test".to_key()); 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/fmt_check.yml: -------------------------------------------------------------------------------- 1 | name: Fmt Check 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main, next ] 8 | schedule: 9 | - cron: '0 23 * * 4' 10 | 11 | env: 12 | RUST_BACKTRACE: full 13 | 14 | jobs: 15 | fmt_check: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v5 19 | - name: Setup Rust 20 | uses: dtolnay/rust-toolchain@stable 21 | - uses: extractions/setup-just@v3 22 | - uses: hustcer/setup-nu@v3.20 23 | with: 24 | version: '0.105.1' 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.PAT_GLOBAL }} 27 | - name: Just version 28 | run: just --version 29 | - name: Fmt Check 30 | run: just fmt_check -------------------------------------------------------------------------------- /.github/workflows/clippy_check.yml: -------------------------------------------------------------------------------- 1 | name: Clippy Check 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main, next ] 8 | schedule: 9 | - cron: '0 23 * * 4' 10 | 11 | env: 12 | RUST_BACKTRACE: full 13 | 14 | jobs: 15 | clippy_check: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v5 19 | - name: Setup Rust 20 | uses: dtolnay/rust-toolchain@stable 21 | - uses: extractions/setup-just@v3 22 | - uses: hustcer/setup-nu@v3.20 23 | with: 24 | version: '0.105.1' 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.PAT_GLOBAL }} 27 | - name: Just version 28 | run: just --version 29 | - name: Clippy Check 30 | run: just clippy_check -------------------------------------------------------------------------------- /src/watch/request.rs: -------------------------------------------------------------------------------- 1 | use crate::db_type::{Key, KeyDefinition, KeyEntry, KeyOptions}; 2 | use std::collections::HashMap; 3 | 4 | #[derive(Clone)] 5 | pub struct WatcherRequest { 6 | // TODO: Maybe replace table_name by KeyDefinition<()> or other 7 | pub(crate) table_name: String, 8 | pub(crate) primary_key: Key, 9 | pub(crate) secondary_keys_value: HashMap, KeyEntry>, 10 | } 11 | 12 | impl WatcherRequest { 13 | pub fn new( 14 | table_name: String, 15 | primary_key: Key, 16 | secondary_keys: HashMap, KeyEntry>, 17 | ) -> Self { 18 | Self { 19 | table_name, 20 | primary_key, 21 | secondary_keys_value: secondary_keys, 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/pr_wait.yml: -------------------------------------------------------------------------------- 1 | name: wait-for-workflows (PR) 2 | 3 | on: 4 | pull_request_target: # secrets are available 5 | branches: [ main, next ] 6 | 7 | permissions: 8 | checks: read 9 | actions: read # read-only is enough 10 | 11 | jobs: 12 | wait-for-workflows: # <-- job name becomes the required check 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 45 15 | steps: 16 | - uses: int128/wait-for-workflows-action@v1.47.0 17 | with: 18 | # PAT inside your repo, fallback to the read-only token if missing 19 | github-token: ${{ secrets.PAT_GLOBAL || github.token }} 20 | filter-workflow-events: pull_request 21 | # the commit we’re validating is the HEAD of the PR 22 | sha: ${{ github.event.pull_request.head.sha }} 23 | -------------------------------------------------------------------------------- /src/db_type/to_input.rs: -------------------------------------------------------------------------------- 1 | use crate::Key; 2 | 3 | use super::{Input, KeyDefinition, KeyEntry, KeyOptions, Result}; 4 | 5 | pub trait ToInput: Sized + native_model::Model { 6 | fn native_db_model() -> crate::Model; 7 | fn native_db_primary_key(&self) -> Key; 8 | fn native_db_secondary_keys( 9 | &self, 10 | ) -> std::collections::HashMap, KeyEntry>; 11 | fn native_db_bincode_encode_to_vec(&self) -> Result>; 12 | fn native_db_bincode_decode_from_slice(slice: &[u8]) -> Result; 13 | 14 | fn native_db_input(&self) -> Result { 15 | Ok(Input { 16 | primary_key: self.native_db_primary_key(), 17 | secondary_keys: self.native_db_secondary_keys(), 18 | value: self.native_db_bincode_encode_to_vec()?, 19 | }) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /cargo_publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # How to test: 4 | # - Use docker `docker run -it --rm -v $(pwd):/mnt/native_db rust:bullseye bash` 5 | # - `cd /mnt/native_db` 6 | # - `export CARGO_TOKEN=` 7 | # - `./cargo_publish.sh` 8 | 9 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 10 | 11 | set -e 12 | set -x 13 | 14 | ARG_TOKEN="--token=$CARGO_TOKEN" 15 | 16 | cd $DIR/native_db_macro 17 | 18 | # Temporarily disable 'set -e' to handle the error manually 19 | set +e 20 | OUTPUT=$(cargo publish $ARG_TOKEN "$@" 2>&1) 21 | EXIT_CODE=$? 22 | set -e 23 | 24 | if [ $EXIT_CODE -ne 0 ]; then 25 | if echo "$OUTPUT" | grep -q "crate version .* is already uploaded"; then 26 | echo "Warning: $OUTPUT" 27 | else 28 | echo "Error: $OUTPUT" 29 | exit $EXIT_CODE 30 | fi 31 | fi 32 | 33 | cd $DIR 34 | cargo publish $ARG_TOKEN $@ -------------------------------------------------------------------------------- /src/db_type/output.rs: -------------------------------------------------------------------------------- 1 | use crate::db_type::{Result, ToInput}; 2 | 3 | use super::Input; 4 | 5 | #[derive(Clone, Debug)] 6 | pub(crate) struct Output(pub(crate) Vec); 7 | 8 | impl From for Output { 9 | fn from(input: Input) -> Self { 10 | Self(input.value) 11 | } 12 | } 13 | 14 | impl From<&[u8]> for Output { 15 | fn from(slice: &[u8]) -> Self { 16 | Self(slice.to_vec()) 17 | } 18 | } 19 | 20 | impl Output { 21 | pub fn inner(&self) -> Result { 22 | T::native_db_bincode_decode_from_slice(&self.0) 23 | } 24 | } 25 | 26 | pub(crate) fn unwrap_item( 27 | item: Option>, 28 | ) -> Option> { 29 | if let Some(item) = item { 30 | let item = item.value(); 31 | let item = T::native_db_bincode_decode_from_slice(item); 32 | Some(item) 33 | } else { 34 | None 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/watch/batch.rs: -------------------------------------------------------------------------------- 1 | use crate::watch::{Event, WatcherRequest}; 2 | use std::fmt::Debug; 3 | 4 | #[derive(Clone)] 5 | pub struct Batch(Vec<(WatcherRequest, Event)>); 6 | 7 | impl Batch { 8 | pub(crate) fn new() -> Self { 9 | Self(Vec::new()) 10 | } 11 | 12 | pub(crate) fn add(&mut self, watcher_request: WatcherRequest, event: Event) { 13 | self.0.push((watcher_request, event)); 14 | } 15 | } 16 | 17 | impl Iterator for Batch { 18 | type Item = (WatcherRequest, Event); 19 | 20 | fn next(&mut self) -> Option { 21 | self.0.pop() 22 | } 23 | } 24 | 25 | impl Debug for Batch { 26 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 27 | write!(f, "[")?; 28 | for (watcher_request, event) in &self.0 { 29 | write!(f, "({:?}, {:?}), ", watcher_request.primary_key, event)?; 30 | } 31 | write!(f, "]") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/check_integrity.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | 6 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] 7 | #[native_model(id = 1, version = 1)] 8 | #[native_db] 9 | struct Item { 10 | #[primary_key] 11 | id: u32, 12 | name: String, 13 | } 14 | 15 | #[test] 16 | fn test_check_integrity() { 17 | let tf = TmpFs::new().unwrap(); 18 | let db_path = tf.path("test"); 19 | 20 | let mut models = Models::new(); 21 | models.define::().unwrap(); 22 | let mut db = Builder::new().create(&models, db_path.clone()).unwrap(); 23 | 24 | // Insert 1 item 25 | let rw = db.rw_transaction().unwrap(); 26 | rw.insert(Item { 27 | id: 1, 28 | name: "test".to_string(), 29 | }) 30 | .unwrap(); 31 | rw.commit().unwrap(); 32 | 33 | let out = db.check_integrity().unwrap(); 34 | assert!(out); 35 | } 36 | -------------------------------------------------------------------------------- /src/transaction/query/drain.rs: -------------------------------------------------------------------------------- 1 | use crate::db_type::{KeyOptions, Result, ToInput, ToKeyDefinition}; 2 | use crate::transaction::internal::rw_transaction::InternalRwTransaction; 3 | 4 | pub struct RwDrain<'db, 'txn> { 5 | pub(crate) internal: &'txn InternalRwTransaction<'db>, 6 | } 7 | 8 | impl RwDrain<'_, '_> { 9 | /// Drain all items. 10 | /// 11 | /// **TODO: needs to be improved, so don't use it yet.** 12 | pub fn primary(&self) -> Result> { 13 | let model = T::native_db_model(); 14 | let out = self.internal.concrete_primary_drain(model)?; 15 | let out = out 16 | .into_iter() 17 | .map(|b| b.inner()) 18 | .collect::>>()?; 19 | Ok(out) 20 | } 21 | 22 | /// Drain all items with a given secondary key. 23 | /// 24 | /// **TODO: needs to be implemented** 25 | pub fn secondary(&self, _key_def: impl ToKeyDefinition) { 26 | todo!() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/build_test_windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main, next ] 8 | schedule: 9 | # At 23:00 on Thursday. 10 | - cron: '0 23 * * 4' 11 | 12 | env: 13 | RUST_BACKTRACE: full 14 | 15 | jobs: 16 | build_test: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: true 20 | matrix: 21 | os: [windows-latest] 22 | toolchain: [stable] 23 | steps: 24 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 25 | - name: Setup Rust 26 | uses: dtolnay/rust-toolchain@stable 27 | with: 28 | toolchain: ${{ matrix.toolchain }} 29 | - uses: extractions/setup-just@v3 30 | - uses: hustcer/setup-nu@v3.20 31 | with: 32 | version: '0.105.1' 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.PAT_GLOBAL }} 35 | - name: Just version 36 | run: just --version 37 | - name: Build 38 | run: just build_all 39 | - name: Test 40 | run: just test_all -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Vincent Herlemont 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/build_test_macos.yml: -------------------------------------------------------------------------------- 1 | name: macOS 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main, next ] 8 | schedule: 9 | # At 23:00 on Thursday. 10 | - cron: '0 23 * * 4' 11 | 12 | env: 13 | RUST_BACKTRACE: full 14 | 15 | jobs: 16 | build_test: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: true 20 | matrix: 21 | os: [macos-13] 22 | toolchain: [stable] 23 | steps: 24 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 25 | - name: Setup Rust 26 | uses: dtolnay/rust-toolchain@stable 27 | with: 28 | toolchain: ${{ matrix.toolchain }} 29 | - uses: extractions/setup-just@v3 30 | - uses: hustcer/setup-nu@v3.20 31 | with: 32 | version: '0.105.1' 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.PAT_GLOBAL }} 35 | - name: Just version 36 | run: just --version 37 | - name: Build 38 | run: just build_all 39 | - name: cat cargo.toml 40 | run: cat Cargo.toml 41 | - name: Test 42 | run: just test_all -------------------------------------------------------------------------------- /tests/snapshot.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | 6 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 7 | #[native_model(id = 1, version = 1)] 8 | #[native_db] 9 | struct Item { 10 | #[primary_key] 11 | id: u32, 12 | name: String, 13 | } 14 | 15 | #[test] 16 | fn test_snapshot() { 17 | let tf = TmpFs::new().unwrap(); 18 | let mut models = Models::new(); 19 | models.define::().unwrap(); 20 | 21 | let db = Builder::new().create_in_memory(&models).unwrap(); 22 | 23 | let rw = db.rw_transaction().unwrap(); 24 | rw.insert(Item { 25 | id: 1, 26 | name: "test".to_string(), 27 | }) 28 | .unwrap(); 29 | rw.commit().unwrap(); 30 | 31 | let db_snapshot = db 32 | .snapshot(&models, tf.path("snapshot.db").as_std_path()) 33 | .unwrap(); 34 | 35 | let r = db_snapshot.r_transaction().unwrap(); 36 | let result_item = r.get().primary(1u32).unwrap().unwrap(); 37 | assert_eq!( 38 | Item { 39 | id: 1, 40 | name: "test".to_string() 41 | }, 42 | result_item 43 | ); 44 | 45 | tf.display_dir_entries(); 46 | } 47 | -------------------------------------------------------------------------------- /src/model.rs: -------------------------------------------------------------------------------- 1 | use crate::db_type::{Error, KeyDefinition, KeyOptions, Result}; 2 | use std::collections::HashSet; 3 | 4 | /// See the documentation [crate::Models::define] to see how to define a model. 5 | #[derive(Clone, Debug)] 6 | pub struct Model { 7 | pub primary_key: KeyDefinition<()>, 8 | pub secondary_keys: HashSet>, 9 | } 10 | 11 | impl Model { 12 | pub fn check_secondary_options( 13 | &self, 14 | secondary_key: &KeyDefinition, 15 | check: F, 16 | ) -> Result<()> 17 | where 18 | F: Fn(KeyOptions) -> bool, 19 | { 20 | let key = self.secondary_keys.get(secondary_key).ok_or_else(|| { 21 | Error::SecondaryKeyDefinitionNotFound { 22 | table: self.primary_key.unique_table_name.to_string(), 23 | key: secondary_key.unique_table_name.clone(), 24 | } 25 | })?; 26 | 27 | if check(key.options.clone()) { 28 | Ok(()) 29 | } else { 30 | Err(Error::SecondaryKeyConstraintMismatch { 31 | table: self.primary_key.unique_table_name.to_string(), 32 | key: secondary_key.unique_table_name.clone(), 33 | got: key.options.clone(), 34 | }) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/watch_tokio.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "tokio")] 2 | 3 | use native_db::*; 4 | use native_db::{watch::Event, Models}; 5 | use native_model::{native_model, Model}; 6 | use serde::{Deserialize, Serialize}; 7 | use shortcut_assert_fs::TmpFs; 8 | 9 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] 10 | #[native_model(id = 1, version = 1)] 11 | #[native_db] 12 | struct ItemA { 13 | #[primary_key] 14 | id: u32, 15 | } 16 | 17 | #[tokio::test] 18 | async fn watch_one_primary_key() { 19 | let tf = TmpFs::new().unwrap(); 20 | 21 | let mut models = Models::new(); 22 | models.define::().unwrap(); 23 | let db = Builder::new() 24 | .create(&models, tf.path("test").as_std_path()) 25 | .unwrap(); 26 | 27 | let a = ItemA { id: 1 }; 28 | 29 | let (mut recv, _) = db.watch().get().primary::(a.id).unwrap(); 30 | 31 | let tx = db.rw_transaction().unwrap(); 32 | tx.insert(a.clone()).unwrap(); 33 | tx.commit().unwrap(); 34 | 35 | for _ in 0..1 { 36 | let inner_event: ItemA = if let Event::Insert(event) = recv.recv().await.unwrap() { 37 | event.inner().unwrap() 38 | } else { 39 | panic!("wrong event") 40 | }; 41 | assert_eq!(inner_event, a); 42 | } 43 | assert!(recv.try_recv().is_err()); 44 | } 45 | 46 | // TODO: maybe do others tests but it should the same as a std::sync::mpsc::channel. 47 | -------------------------------------------------------------------------------- /tests/compact.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | 6 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] 7 | #[native_model(id = 1, version = 1)] 8 | #[native_db] 9 | struct Item { 10 | #[primary_key] 11 | id: u32, 12 | name: String, 13 | } 14 | 15 | #[test] 16 | fn test_compact() { 17 | let tf = TmpFs::new().unwrap(); 18 | let db_path = tf.path("test"); 19 | 20 | let mut models = Models::new(); 21 | models.define::().unwrap(); 22 | let mut db = Builder::new().create(&models, db_path.clone()).unwrap(); 23 | 24 | // Insert 1000 items 25 | let rw = db.rw_transaction().unwrap(); 26 | for i in 0..999 { 27 | rw.insert(Item { 28 | id: i, 29 | name: format!("test_{}", i), 30 | }) 31 | .unwrap(); 32 | } 33 | rw.commit().unwrap(); 34 | 35 | // Check the size of the database 36 | let metadata = std::fs::metadata(db_path.clone()).unwrap(); 37 | let file_size = metadata.len(); 38 | assert_eq!(file_size, 585728); 39 | dbg!(file_size); 40 | 41 | let out = db.compact().unwrap(); 42 | assert!(out); 43 | 44 | // Check the size of the compacted database 45 | let metadata = std::fs::metadata(db_path.clone()).unwrap(); 46 | let file_size = metadata.len(); 47 | assert_eq!(file_size, 98304); 48 | } 49 | -------------------------------------------------------------------------------- /tests/query/insert_len_pk.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | 6 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 7 | #[native_model(id = 1, version = 1)] 8 | #[native_db] 9 | struct Item { 10 | #[primary_key] 11 | id: u32, 12 | name: String, 13 | } 14 | 15 | #[test] 16 | fn insert_len_read_transaction() { 17 | let tf = TmpFs::new().unwrap(); 18 | 19 | let item = Item { 20 | id: 1, 21 | name: "test".to_string(), 22 | }; 23 | 24 | let mut models = Models::new(); 25 | models.define::().unwrap(); 26 | let db = Builder::new() 27 | .create(&models, tf.path("test").as_std_path()) 28 | .unwrap(); 29 | 30 | let rw = db.rw_transaction().unwrap(); 31 | rw.insert(item.clone()).unwrap(); 32 | rw.commit().unwrap(); 33 | 34 | let r = db.r_transaction().unwrap(); 35 | let result_item = r.len().primary::().unwrap(); 36 | assert_eq!(1, result_item); 37 | 38 | let item = Item { 39 | id: 2, 40 | name: "test".to_string(), 41 | }; 42 | 43 | let rw = db.rw_transaction().unwrap(); 44 | rw.insert(item.clone()).unwrap(); 45 | rw.commit().unwrap(); 46 | 47 | let r = db.r_transaction().unwrap(); 48 | let result_item = r.len().primary::().unwrap(); 49 | assert_eq!(2, result_item); 50 | } 51 | -------------------------------------------------------------------------------- /examples/major_upgrade/src/main_old.rs: -------------------------------------------------------------------------------- 1 | use crate::models::v08x::V08xModel; 2 | use native_db_v0_8_x::{Builder, Models}; 3 | use std::path::Path; 4 | 5 | pub fn main_old>(db_path: P) -> Result<(), Box> { 6 | let db_path = db_path.as_ref(); 7 | 8 | // Clean up if exists 9 | if db_path.exists() { 10 | std::fs::remove_file(db_path).ok(); 11 | } 12 | 13 | // Define the model 14 | let mut models = Models::new(); 15 | models.define::().map_err(Box::new)?; 16 | 17 | // Create database with v0.8.x - path is a file path 18 | let db = Builder::new().create(&models, db_path).map_err(Box::new)?; 19 | 20 | // Insert some test data 21 | let rw = db.rw_transaction().map_err(Box::new)?; 22 | rw.insert(V08xModel { 23 | id: 1, 24 | name: "Test Item v0.8.x".to_string(), 25 | }) 26 | .map_err(Box::new)?; 27 | rw.insert(V08xModel { 28 | id: 2, 29 | name: "Another Item v0.8.x".to_string(), 30 | }) 31 | .map_err(Box::new)?; 32 | rw.commit().map_err(Box::new)?; 33 | 34 | // Verify data 35 | let r = db.r_transaction().map_err(Box::new)?; 36 | let item: V08xModel = r.get().primary(1u32).map_err(Box::new)?.unwrap(); 37 | assert_eq!(item.id, 1); 38 | assert_eq!(item.name, "Test Item v0.8.x"); 39 | 40 | println!("Successfully created v0.8.x database at: {db_path:?}"); 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /tests/query/insert_len_sk.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | 6 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 7 | #[native_model(id = 1, version = 1)] 8 | #[native_db] 9 | struct Item { 10 | #[primary_key] 11 | id: u32, 12 | #[secondary_key(optional)] 13 | name: Option, 14 | } 15 | 16 | #[test] 17 | fn insert_len_read_transaction() { 18 | let tf = TmpFs::new().unwrap(); 19 | 20 | let item = Item { id: 1, name: None }; 21 | 22 | let mut models = Models::new(); 23 | models.define::().unwrap(); 24 | let db = Builder::new() 25 | .create(&models, tf.path("test").as_std_path()) 26 | .unwrap(); 27 | 28 | let rw = db.rw_transaction().unwrap(); 29 | rw.insert(item.clone()).unwrap(); 30 | rw.commit().unwrap(); 31 | 32 | let r = db.r_transaction().unwrap(); 33 | let result_item = r.len().secondary::(ItemKey::name).unwrap(); 34 | assert_eq!(0, result_item); 35 | 36 | let item = Item { 37 | id: 2, 38 | name: Some("test".to_string()), 39 | }; 40 | 41 | let rw = db.rw_transaction().unwrap(); 42 | rw.insert(item.clone()).unwrap(); 43 | rw.commit().unwrap(); 44 | 45 | let r = db.r_transaction().unwrap(); 46 | let result_item = r.len().secondary::(ItemKey::name).unwrap(); 47 | assert_eq!(1, result_item); 48 | } 49 | -------------------------------------------------------------------------------- /tests/query/insert_remove_pk.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | 6 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 7 | #[native_model(id = 1, version = 1)] 8 | #[native_db] 9 | struct Item { 10 | #[primary_key] 11 | id: u32, 12 | name: String, 13 | } 14 | 15 | #[test] 16 | fn insert_get() { 17 | let tf = TmpFs::new().unwrap(); 18 | 19 | let item = Item { 20 | id: 1, 21 | name: "test".to_string(), 22 | }; 23 | 24 | let mut models = Models::new(); 25 | models.define::().unwrap(); 26 | let db = Builder::new() 27 | .create(&models, tf.path("test").as_std_path()) 28 | .unwrap(); 29 | 30 | let rw = db.rw_transaction().unwrap(); 31 | rw.insert(item.clone()).unwrap(); 32 | rw.commit().unwrap(); 33 | 34 | let stats = db.redb_stats().unwrap(); 35 | assert_eq!(stats.primary_tables.len(), 1); 36 | assert_eq!(stats.primary_tables[0].name, "1_1_id"); 37 | assert_eq!(stats.primary_tables[0].n_entries, Some(1)); 38 | 39 | let rw = db.rw_transaction().unwrap(); 40 | let old_value = rw.remove(item.clone()).unwrap(); 41 | assert_eq!(old_value, item); 42 | rw.commit().unwrap(); 43 | 44 | let stats = db.redb_stats().unwrap(); 45 | assert_eq!(stats.primary_tables.len(), 1); 46 | assert_eq!(stats.primary_tables[0].name, "1_1_id"); 47 | assert_eq!(stats.primary_tables[0].n_entries, Some(0)); 48 | } 49 | -------------------------------------------------------------------------------- /examples/major_upgrade/src/models/current_version.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | // Import current version as native_db for macro expansion 4 | use native_db_current as native_db; 5 | use native_db_current::ToKey; 6 | 7 | // Import native_model macro version matched with the current version native_db for macro expansion. 8 | use native_model_current as native_model; 9 | use native_model_current::{native_model, Model}; 10 | 11 | // Model for current version 12 | #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] 13 | // We no need to add the `from` attribute here, we manually implement 14 | // conversion between the two models using `.into()` method. 15 | // Maybe we could reset the version number too. And set it to 1. 16 | #[native_model(id = 1, version = 1)] 17 | #[native_db_current::native_db] 18 | pub struct CurrentModel { 19 | #[primary_key] 20 | pub id: u32, 21 | pub name: String, 22 | } 23 | 24 | // Upgrade from v0.8.x to current version 25 | impl From for CurrentModel { 26 | fn from(v08x_model: crate::models::v08x::V08xModel) -> Self { 27 | Self { 28 | id: v08x_model.id, 29 | name: v08x_model.name, 30 | } 31 | } 32 | } 33 | 34 | // Downgrade from current version to v0.8.x 35 | impl From for crate::models::v08x::V08xModel { 36 | fn from(current_model: CurrentModel) -> Self { 37 | Self { 38 | id: current_model.id, 39 | name: current_model.name, 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/transaction/r_transaction.rs: -------------------------------------------------------------------------------- 1 | use crate::transaction::internal::r_transaction::InternalRTransaction; 2 | use crate::transaction::query::RGet; 3 | use crate::transaction::query::RLen; 4 | use crate::transaction::query::RScan; 5 | 6 | pub struct RTransaction<'db> { 7 | pub(crate) internal: InternalRTransaction<'db>, 8 | } 9 | 10 | impl<'db> RTransaction<'db> { 11 | /// Get a value from the database. 12 | /// 13 | /// - [`primary`](crate::transaction::query::RGet::primary) - Get a item by primary key. 14 | /// - [`secondary`](crate::transaction::query::RGet::secondary) - Get a item by secondary key. 15 | pub fn get<'txn>(&'txn self) -> RGet<'db, 'txn> { 16 | RGet { 17 | internal: &self.internal, 18 | } 19 | } 20 | 21 | /// Get values from the database. 22 | /// 23 | /// - [`primary`](crate::transaction::query::RScan::primary) - Scan items by primary key. 24 | /// - [`secondary`](crate::transaction::query::RScan::secondary) - Scan items by secondary key. 25 | pub fn scan<'txn>(&'txn self) -> RScan<'db, 'txn> { 26 | RScan { 27 | internal: &self.internal, 28 | } 29 | } 30 | 31 | /// Get the number of values in the database. 32 | /// 33 | /// - [`primary`](crate::transaction::query::RLen::primary) - Get the number of items by primary key. 34 | /// - [`secondary`](crate::transaction::query::RLen::secondary) - Get the number of items by secondary key. 35 | pub fn len<'txn>(&'txn self) -> RLen<'db, 'txn> { 36 | RLen { 37 | internal: &self.internal, 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/db_type/input.rs: -------------------------------------------------------------------------------- 1 | use crate::db_type::{Error, Key, KeyDefinition, KeyEntry, KeyOptions, Result}; 2 | 3 | #[derive(Debug, Clone)] 4 | pub struct Input { 5 | pub(crate) primary_key: Key, 6 | pub(crate) secondary_keys: std::collections::HashMap, KeyEntry>, 7 | pub(crate) value: Vec, 8 | } 9 | 10 | impl Input { 11 | pub(crate) fn secondary_key_value( 12 | &self, 13 | secondary_key_def: &KeyDefinition, 14 | ) -> Result { 15 | let secondary_key = self.secondary_keys.get(secondary_key_def).ok_or( 16 | Error::SecondaryKeyDefinitionNotFound { 17 | table: "".to_string(), 18 | key: secondary_key_def.unique_table_name.clone(), 19 | }, 20 | )?; 21 | let out = if !secondary_key_def.options.unique { 22 | match secondary_key { 23 | KeyEntry::Default(value) => { 24 | // KeyEntry::Default(composite_key(value, &self.primary_key)) 25 | KeyEntry::Default(value.to_owned()) 26 | } 27 | KeyEntry::Optional(value) => { 28 | // let value = value 29 | // .as_ref() 30 | // .map(|value| composite_key(value, &self.primary_key)); 31 | // KeyEntry::Optional(value) 32 | KeyEntry::Optional(value.as_ref().map(|value| value.to_owned())) 33 | } 34 | } 35 | } else { 36 | secondary_key.clone() 37 | }; 38 | Ok(out) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/watch/query/mod.rs: -------------------------------------------------------------------------------- 1 | mod get; 2 | mod internal; 3 | mod scan; 4 | 5 | pub use get::*; 6 | pub(crate) use internal::*; 7 | pub use scan::*; 8 | 9 | /// Watch queries. 10 | /// 11 | /// **Memory Warning**: Each active watcher consumes memory until explicitly removed. The watch 12 | /// system stores all watchers in a HashMap and keeps channel senders alive. With the `tokio` 13 | /// feature, unbounded channels are used which can accumulate events if not consumed. 14 | /// 15 | /// Best practices: 16 | /// - Always call [`Database::unwatch()`](crate::Database::unwatch) when done watching 17 | /// - Consume events promptly to prevent channel backlog 18 | /// - Consider implementing a cleanup strategy for long-running applications 19 | pub struct Watch<'db> { 20 | pub(crate) internal: InternalWatch<'db>, 21 | } 22 | 23 | impl<'db> Watch<'db> { 24 | /// Watch only one value. 25 | /// 26 | /// - [`primary`](crate::watch::query::WatchGet::primary) - Watch a item by primary key. 27 | /// - [`secondary`](crate::watch::query::WatchGet::secondary) - Watch a item by secondary key. 28 | pub fn get<'w>(&'w self) -> WatchGet<'db, 'w> { 29 | WatchGet { 30 | internal: &self.internal, 31 | } 32 | } 33 | /// Watch multiple values. 34 | /// 35 | /// - [`primary`](crate::watch::query::WatchScan::primary) - Watch items by primary key. 36 | /// - [`secondary`](crate::watch::query::WatchScan::secondary) - Watch items by secondary key. 37 | pub fn scan<'w>(&'w self) -> WatchScan<'db, 'w> { 38 | WatchScan { 39 | internal: &self.internal, 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/query/upsert_get_pk.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | 6 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 7 | #[native_model(id = 1, version = 1)] 8 | #[native_db] 9 | struct Item { 10 | #[primary_key] 11 | id: u32, 12 | name: String, 13 | } 14 | 15 | #[test] 16 | fn upsert_get() { 17 | let item = Item { 18 | id: 1, 19 | name: "test".to_string(), 20 | }; 21 | 22 | let tf = TmpFs::new().unwrap(); 23 | let mut models = Models::new(); 24 | models.define::().unwrap(); 25 | let db = Builder::new() 26 | .create(&models, tf.path("test").as_std_path()) 27 | .unwrap(); 28 | 29 | let rw = db.rw_transaction().unwrap(); 30 | rw.upsert(item.clone()).unwrap(); 31 | rw.commit().unwrap(); 32 | 33 | let r = db.r_transaction().unwrap(); 34 | let result_item = r.get().primary(1u32).unwrap().unwrap(); 35 | assert_eq!(item, result_item); 36 | } 37 | 38 | #[test] 39 | fn test_upsert_duplicate_key() { 40 | let tf = TmpFs::new().unwrap(); 41 | 42 | let item_1 = Item { 43 | id: 1, 44 | name: "test".to_string(), 45 | }; 46 | 47 | let mut models = Models::new(); 48 | models.define::().unwrap(); 49 | let db = Builder::new() 50 | .create(&models, tf.path("test").as_std_path()) 51 | .unwrap(); 52 | 53 | let rw = db.rw_transaction().unwrap(); 54 | rw.upsert(item_1.clone()).unwrap(); 55 | let result = rw.upsert(item_1.clone()); 56 | assert!(result.is_ok()); 57 | } 58 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "semanticCommits": "enabled", 4 | "semanticCommitType": "chore", 5 | "semanticCommitScope": "deps", 6 | "platformAutomerge": true, 7 | "packageRules": [ 8 | { 9 | "matchDepNames": ["redb1"], 10 | "enabled": false 11 | }, 12 | { 13 | "description": "Automerge non-major updates", 14 | "matchUpdateTypes": ["minor", "patch"], 15 | "automerge": true 16 | }, 17 | { 18 | "matchPackagePatterns": ["thiserror", "chrono", "tokio", "serde", "syn", "quote", "proc-macro2", "include_dir", "semver"], 19 | "matchUpdateTypes": ["patch"], 20 | "enabled": false 21 | }, 22 | { 23 | "matchPackagePatterns": ["*"], 24 | "rangeStrategy": "bump" 25 | }, 26 | { 27 | "description": "Automerge actions", 28 | "matchDepTypes": ["action"], 29 | "matchUpdateTypes": ["major", "minor", "patch"], 30 | "automerge": true 31 | } 32 | ], 33 | "regexManagers": [ 34 | { 35 | "fileMatch": ["^README\\.md$"], 36 | "matchStrings": [ 37 | "\"native_model\" = \"(?.*?)\"" 38 | ], 39 | "depNameTemplate": "native_model", 40 | "datasourceTemplate": "crate", 41 | "versioningTemplate": "semver" 42 | }, 43 | { 44 | "fileMatch": ["^\\.github/workflows/[^/]+\\.ya?ml$"], 45 | "matchStrings": ["uses: hustcer/setup-nu@.*?\n.*?version: '\\s*(?.*?)'"], 46 | "depNameTemplate": "nushell", 47 | "datasourceTemplate": "github-releases", 48 | "packageNameTemplate": "nushell/nushell" 49 | } 50 | ], 51 | "enabled": true 52 | } -------------------------------------------------------------------------------- /tests/native_model.rs: -------------------------------------------------------------------------------- 1 | use bincode::{config, Decode, Encode}; 2 | use native_db::*; 3 | use native_db_macro::native_db; 4 | use native_model::{native_model, Model}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | pub struct Bincode; 8 | 9 | impl native_model::Encode for Bincode { 10 | type Error = bincode::error::EncodeError; 11 | fn encode(obj: &T) -> std::result::Result, bincode::error::EncodeError> { 12 | bincode::encode_to_vec(obj, config::standard()) 13 | } 14 | } 15 | 16 | impl> native_model::Decode for Bincode { 17 | type Error = bincode::error::DecodeError; 18 | fn decode(data: Vec) -> std::result::Result { 19 | bincode::decode_from_slice(&data, config::standard()).map(|(result, _)| result) 20 | } 21 | } 22 | 23 | #[derive(Serialize, Deserialize, Encode, Decode, Eq, PartialEq, Debug)] 24 | #[native_model(id = 1, version = 1, with = Bincode)] 25 | #[native_db(primary_key(compute_primary_key -> Vec))] 26 | struct ItemV1 { 27 | id: u32, 28 | name: String, 29 | } 30 | 31 | impl ItemV1 { 32 | #[allow(dead_code)] 33 | pub fn compute_primary_key(&self) -> Vec { 34 | format!("{}-{}", self.id, self.name).into() 35 | } 36 | } 37 | 38 | #[test] 39 | fn test_native_encode() { 40 | let my_item = ItemV1 { 41 | id: 1, 42 | name: "test".to_string(), 43 | }; 44 | 45 | let my_item_packed = native_model::encode(&my_item).unwrap(); 46 | let (my_item_unpacked, _) = native_model::decode::(my_item_packed).unwrap(); 47 | assert_eq!(my_item, my_item_unpacked); 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/build_test_ios.yml: -------------------------------------------------------------------------------- 1 | name: iOS 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main, next ] 8 | schedule: 9 | # At 23:00 on Thursday. 10 | - cron: '0 23 * * 4' 11 | 12 | env: 13 | RUST_BACKTRACE: full 14 | 15 | jobs: 16 | build_test: 17 | runs-on: macos-13 18 | strategy: 19 | fail-fast: true 20 | matrix: 21 | toolchain: [stable] 22 | device: ["iPhone 14"] 23 | steps: 24 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 25 | - name: Setup Rust 26 | uses: dtolnay/rust-toolchain@stable 27 | with: 28 | toolchain: ${{ matrix.toolchain }} 29 | targets: x86_64-apple-ios 30 | # Install utilities 31 | - name: Cache cargo install 32 | uses: actions/cache@v4 33 | if: always() 34 | with: 35 | path: | 36 | ~/.cargo/bin/ 37 | key: cargo-global-${{ matrix.toolchain }}-${{ github.ref }}-${{ hashFiles('**/Cargo.lock') }} 38 | - run: if ! command -v cargo-dinghy &> /dev/null; then cargo install --version 0.8.1 cargo-dinghy; fi 39 | - run: if ! command -v just &> /dev/null; then cargo install --version 1.25.2 just; fi 40 | - run: just --version 41 | - uses: hustcer/setup-nu@v3.20 42 | with: 43 | version: '0.105.1' 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.PAT_GLOBAL }} 46 | # End install utilities 47 | - run: just test_ios_launch_simulator "${{ matrix.device }}" 48 | # - run: just test_ios_list_simulators 49 | # Skip doctests on iOS due to cargo-dinghy issues with doctest packaging (Info.plist/string indexing panics) 50 | - run: just test_ios_lib -------------------------------------------------------------------------------- /src/metadata/metadata.rs: -------------------------------------------------------------------------------- 1 | use super::CURRENT_NATIVE_MODEL_VERSION; 2 | use super::CURRENT_VERSION; 3 | use semver::Version; 4 | 5 | pub struct Metadata { 6 | current_version: String, 7 | current_native_model_version: String, 8 | previous_version: Option, 9 | previous_native_model_version: Option, 10 | } 11 | 12 | impl Metadata { 13 | pub(crate) fn from_stored(stored_version: String, stored_native_model_version: String) -> Self { 14 | Self { 15 | current_version: stored_version, 16 | current_native_model_version: stored_native_model_version, 17 | previous_version: None, 18 | previous_native_model_version: None, 19 | } 20 | } 21 | 22 | pub fn current_version(&self) -> &str { 23 | &self.current_version 24 | } 25 | 26 | pub fn current_native_model_version(&self) -> &str { 27 | &self.current_native_model_version 28 | } 29 | 30 | pub fn previous_version(&self) -> Option<&str> { 31 | self.previous_version.as_deref() 32 | } 33 | 34 | pub fn previous_native_model_version(&self) -> Option<&str> { 35 | self.previous_native_model_version.as_deref() 36 | } 37 | } 38 | 39 | impl Default for Metadata { 40 | fn default() -> Self { 41 | let current_version = Version::parse(CURRENT_VERSION).unwrap(); 42 | let current_native_model_version = Version::parse(CURRENT_NATIVE_MODEL_VERSION).unwrap(); 43 | 44 | Self { 45 | current_version: current_version.to_string(), 46 | current_native_model_version: current_native_model_version.to_string(), 47 | previous_version: None, 48 | previous_native_model_version: None, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/deserialization_error.rs: -------------------------------------------------------------------------------- 1 | use native_db::db_type::Error; 2 | use native_db::db_type::Result; 3 | use native_db::*; 4 | 5 | use itertools::Itertools; 6 | use native_model::{native_model, Error as ModelError, Model}; 7 | use serde::{Deserialize, Serialize}; 8 | use shortcut_assert_fs::TmpFs; 9 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 10 | #[native_model(id = 1, version = 1)] 11 | #[native_db] 12 | struct Item1 { 13 | // The type of the primary key must be u32, see generation of the test "create_local_database_for_tests". 14 | // We change the type of the primary key to String to generate a deserialization error. 15 | #[primary_key] 16 | id: String, 17 | #[secondary_key(unique)] 18 | name: String, 19 | } 20 | 21 | use include_dir::{include_dir, Dir}; 22 | static PROJECT_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/tests/data"); 23 | 24 | #[cfg(not(any(target_os = "android", target_os = "ios")))] 25 | #[test] 26 | #[ignore = "TODO: Update test to handle version upgrade errors. This test uses old database files (0.7.1) that now trigger upgrade errors."] 27 | fn create_local_database_for_tests() { 28 | let tmp = TmpFs::new().unwrap(); 29 | tmp.copy_assets(&PROJECT_DIR).unwrap(); 30 | tmp.display_dir_entries(); 31 | let database_path = tmp.path("db_0_8-pre-0").to_path_buf(); 32 | 33 | let mut models = Models::new(); 34 | models.define::().unwrap(); 35 | let db = Builder::new().open(&models, &database_path).unwrap(); 36 | let r = db.r_transaction().unwrap(); 37 | let result: Result> = r.scan().primary().unwrap().all().unwrap().try_collect(); 38 | assert!(matches!( 39 | result, 40 | Err(Error::ModelError(boxed_error)) 41 | if matches!(*boxed_error, ModelError::DecodeBodyError(_)) 42 | )); 43 | } 44 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "native_db" 3 | version = "0.8.1" 4 | authors = ["Vincent Herlemont "] 5 | edition = "2021" 6 | description = "Drop-in embedded database" 7 | license = "MIT" 8 | repository = "https://github.com/vincent-herlemont/native_db" 9 | readme = "README.md" 10 | keywords = ["embedded-database", "database", "multi-platform", "android", "ios"] 11 | categories = ["database-implementations", "concurrency", "data-structures", "caching", "algorithms"] 12 | 13 | [workspace] 14 | members = ["native_db_macro"] 15 | 16 | [dependencies] 17 | redb = "=3.0.2" 18 | native_db_macro = { version = "0.8.1", path = "native_db_macro" } 19 | thiserror = "2.0.0" 20 | serde = { version = "1.0" } 21 | native_model = { version = "0.6.2" } 22 | semver = "1" 23 | 24 | # Optional tokio support 25 | tokio = { version = "1.47.1", features = ["sync"], optional = true } 26 | # TODO: channels with futures 27 | # TODO: channels crossbeam 28 | 29 | 30 | [dev-dependencies] 31 | assert_fs = "1.1.3" 32 | serial_test = { version = "3.2.0", features = ["file_locks"] } 33 | shortcut_assert_fs = { version = "0.1.0" } 34 | tokio = { version = "1.47.1", features = ["test-util","macros"] } 35 | bincode = { version = "2.0.1", features = ["serde"] } 36 | criterion = { version = "0.7.0" } 37 | doc-comment = "0.3.3" 38 | uuid = { version = "1.18.1", features = ["serde", "v4"] } 39 | chrono = { version = "0.4", features = ["serde"] } 40 | rand = "0.9.2" 41 | once_cell = "1.21.3" 42 | dinghy-test = "0.8.1" 43 | itertools = "0.14.0" 44 | include_dir = "0.7" 45 | paste = "1.0.15" 46 | cc = "1.2.41" 47 | rusqlite = { version = "0.37.0", features = ["bundled"] } 48 | concat-idents = "1.1.5" 49 | 50 | 51 | [features] 52 | default = [ ] 53 | 54 | [[bench]] 55 | name = "all" 56 | harness = false 57 | 58 | [build-dependencies] 59 | skeptic = "0.13.7" -------------------------------------------------------------------------------- /.github/workflows/build_test_linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main, next ] 8 | schedule: 9 | # At 23:00 on Thursday. 10 | - cron: '0 23 * * 4' 11 | 12 | env: 13 | RUST_BACKTRACE: full 14 | 15 | jobs: 16 | build_bench: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: true 20 | matrix: 21 | os: [ubuntu-latest] 22 | toolchain: [stable] 23 | steps: 24 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 25 | - name: Setup Rust 26 | uses: dtolnay/rust-toolchain@stable 27 | with: 28 | toolchain: ${{ matrix.toolchain }} 29 | - uses: extractions/setup-just@v3 30 | - uses: hustcer/setup-nu@v3.20 31 | with: 32 | version: '0.105.1' 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.PAT_GLOBAL }} 35 | - name: Just version 36 | run: just --version 37 | - name: Build 38 | run: just bench_build 39 | build_test: 40 | runs-on: ${{ matrix.os }} 41 | strategy: 42 | fail-fast: true 43 | matrix: 44 | os: [ubuntu-latest] 45 | toolchain: [stable] 46 | steps: 47 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 48 | - name: Setup Rust 49 | uses: dtolnay/rust-toolchain@stable 50 | with: 51 | toolchain: ${{ matrix.toolchain }} 52 | - uses: extractions/setup-just@v3 53 | - uses: hustcer/setup-nu@v3.20 54 | with: 55 | version: '0.105.1' 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.PAT_GLOBAL }} 58 | - name: Just version 59 | run: just --version 60 | - name: Build 61 | run: just build_all 62 | - name: Test 63 | run: just test_all -------------------------------------------------------------------------------- /tests/query/insert_get_pk.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | 6 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 7 | #[native_model(id = 1, version = 1)] 8 | #[native_db] 9 | struct Item { 10 | #[primary_key] 11 | id: u32, 12 | name: String, 13 | } 14 | 15 | #[test] 16 | fn insert_get() { 17 | let item = Item { 18 | id: 1, 19 | name: "test".to_string(), 20 | }; 21 | 22 | let tf = TmpFs::new().unwrap(); 23 | let mut models = Models::new(); 24 | models.define::().unwrap(); 25 | let db = Builder::new() 26 | .create(&models, tf.path("test").as_std_path()) 27 | .unwrap(); 28 | 29 | let rw = db.rw_transaction().unwrap(); 30 | rw.insert(item.clone()).unwrap(); 31 | rw.commit().unwrap(); 32 | 33 | let r = db.r_transaction().unwrap(); 34 | let result_item = r.get().primary(1u32).unwrap().unwrap(); 35 | assert_eq!(item, result_item); 36 | } 37 | 38 | #[test] 39 | fn test_insert_duplicate_key() { 40 | let tf = TmpFs::new().unwrap(); 41 | 42 | let item_1 = Item { 43 | id: 1, 44 | name: "test1".to_string(), 45 | }; 46 | let item_2 = Item { 47 | id: 1, 48 | name: "test2".to_string(), 49 | }; 50 | 51 | let mut models = Models::new(); 52 | models.define::().unwrap(); 53 | let db = Builder::new() 54 | .create(&models, tf.path("test").as_std_path()) 55 | .unwrap(); 56 | 57 | let rw = db.rw_transaction().unwrap(); 58 | rw.insert(item_1.clone()).unwrap(); 59 | let result = rw.insert(item_2.clone()); 60 | assert!(result.is_err()); 61 | assert!(matches!( 62 | result.unwrap_err(), 63 | db_type::Error::DuplicateKey { .. } 64 | )); 65 | } 66 | -------------------------------------------------------------------------------- /tests/macro_def/export_keys_attribute.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | use native_db::*; 3 | use native_model::{native_model, Model}; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | // TODO somehow test visibility of keys enum from a sibling/child crate? 7 | 8 | /// Test struct to ensure `#[native_db(export_keys = true)]` compiles successfully 9 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] 10 | #[native_model(id = 1, version = 1)] 11 | #[native_db(export_keys = true)] 12 | struct Item { 13 | #[primary_key] 14 | id: u32, 15 | #[secondary_key] 16 | name: String, 17 | } 18 | 19 | #[test] 20 | fn test_insert_my_item() { 21 | let item = Item { 22 | id: 1, 23 | name: "test".to_string(), 24 | }; 25 | 26 | let key = item.native_db_primary_key(); 27 | assert_eq!(key, 1_u32.to_key()); 28 | 29 | let mut models: Models = Models::new(); 30 | models.define::().unwrap(); 31 | 32 | let db = Builder::new().create_in_memory(&models).unwrap(); 33 | 34 | let rw = db.rw_transaction().unwrap(); 35 | rw.insert(Item { 36 | id: 1, 37 | name: "test".to_string(), 38 | }) 39 | .unwrap(); 40 | rw.commit().unwrap(); 41 | 42 | // Get primary key 43 | let r = db.r_transaction().unwrap(); 44 | let result_item: Item = r.get().primary(1u32).unwrap().unwrap(); 45 | assert_eq!(result_item.id, 1); 46 | assert_eq!(result_item.name, "test"); 47 | 48 | // Get secondary key 49 | let r = db.r_transaction().unwrap(); 50 | let result_item: Vec = r 51 | .scan() 52 | .secondary(ItemKey::name) 53 | .unwrap() 54 | .all() 55 | .unwrap() 56 | .try_collect() 57 | .unwrap(); 58 | assert_eq!(result_item.len(), 1); 59 | assert_eq!(result_item[0].id, 1); 60 | assert_eq!(result_item[0].name, "test"); 61 | } 62 | -------------------------------------------------------------------------------- /src/snapshot.rs: -------------------------------------------------------------------------------- 1 | use crate::db_type::Result; 2 | use crate::{Builder, Database, Models}; 3 | use redb::{ReadableDatabase, ReadableMultimapTable, ReadableTable}; 4 | use std::path::Path; 5 | 6 | impl Database<'_> { 7 | pub fn snapshot<'a>(&self, models: &'a Models, path: &Path) -> Result> { 8 | let new_db = Builder::new().create(models, path)?; 9 | let r = self.instance.redb_database()?.begin_read()?; 10 | let w = new_db.instance.redb_database()?.begin_write()?; 11 | { 12 | // Copy primary tables 13 | for primary_table_definition in self.primary_table_definitions.values() { 14 | let table = r.open_table(primary_table_definition.redb)?; 15 | let mut new_table = w.open_table(primary_table_definition.redb)?; 16 | for result in table.iter()? { 17 | let (key, value) = result?; 18 | new_table.insert(key.value(), value.value())?; 19 | } 20 | 21 | // Copy secondary tables 22 | for secondary_table_definition in primary_table_definition.secondary_tables.values() 23 | { 24 | let table = r.open_multimap_table(secondary_table_definition.redb)?; 25 | let mut new_table = w.open_multimap_table(secondary_table_definition.redb)?; 26 | for result in table.iter()? { 27 | let (secondary_key, primary_keys) = result?; 28 | for primary_key in primary_keys { 29 | let primary_key = primary_key?; 30 | new_table.insert(secondary_key.value(), primary_key.value())?; 31 | } 32 | } 33 | } 34 | } 35 | } 36 | w.commit()?; 37 | Ok(new_db) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/primary_drain/only_primary_key.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | 6 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] 7 | #[native_model(id = 1, version = 1)] 8 | #[native_db( 9 | primary_key(generate_my_primary_key -> Vec) 10 | )] 11 | struct Item { 12 | id: u32, 13 | name: String, 14 | } 15 | 16 | impl Item { 17 | pub fn generate_my_primary_key(&self) -> Vec { 18 | self.id.to_le_bytes().to_vec() 19 | } 20 | 21 | pub fn inc(&mut self) -> &Self { 22 | self.id += 1; 23 | self 24 | } 25 | } 26 | 27 | #[test] 28 | fn drain_all() { 29 | let tf = TmpFs::new().unwrap(); 30 | 31 | let mut item = Item { 32 | id: 1, 33 | name: "test".to_string(), 34 | }; 35 | 36 | let mut models = Models::new(); 37 | models.define::().unwrap(); 38 | let db = Builder::new() 39 | .create(&models, tf.path("test").as_std_path()) 40 | .unwrap(); 41 | 42 | // Insert 5 items 43 | let rw = db.rw_transaction().unwrap(); 44 | rw.insert(item.clone()).unwrap(); 45 | rw.insert(item.inc().clone()).unwrap(); 46 | rw.insert(item.inc().clone()).unwrap(); 47 | rw.insert(item.inc().clone()).unwrap(); 48 | rw.insert(item.inc().clone()).unwrap(); 49 | rw.commit().unwrap(); 50 | 51 | // Count items 52 | let r = db.r_transaction().unwrap(); 53 | let len = r.len().primary::().unwrap(); 54 | assert_eq!(len, 5); 55 | 56 | // Drain items 57 | let rw = db.rw_transaction().unwrap(); 58 | let items = rw.drain().primary::().unwrap(); 59 | assert_eq!(items.len(), 5); 60 | rw.commit().unwrap(); 61 | 62 | // Count items 63 | let r = db.r_transaction().unwrap(); 64 | let len = r.len().primary::().unwrap(); 65 | assert_eq!(len, 0); 66 | } 67 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [ main ] # automatic release after merge 6 | workflow_dispatch: # manual release trigger 7 | 8 | permissions: 9 | contents: write # release step will need this 10 | checks: read 11 | actions: read 12 | 13 | jobs: 14 | wait-for-workflows: 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 45 17 | if: github.event_name == 'push' # only wait for workflows on automatic triggers 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v5 21 | - uses: int128/wait-for-workflows-action@v1.47.0 22 | with: 23 | token: ${{ secrets.PAT_GLOBAL }} 24 | filter-workflow-events: push 25 | sha: ${{ github.sha }} 26 | 27 | release: 28 | name: Release 29 | runs-on: ubuntu-latest 30 | needs: [wait-for-workflows] 31 | if: github.ref == 'refs/heads/main' && always() && (needs.wait-for-workflows.result == 'success' || needs.wait-for-workflows.result == 'skipped') 32 | permissions: 33 | contents: write 34 | packages: write 35 | steps: 36 | - name: Checkout code 37 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 38 | with: 39 | ref: main 40 | fetch-depth: 0 41 | 42 | - name: install npm 43 | uses: actions/setup-node@v5 44 | with: 45 | node-version: '22.19.0' 46 | 47 | - name: Semantic Release 48 | uses: cycjimmy/semantic-release-action@v4 49 | with: 50 | dry_run: ${{ github.event_name != 'workflow_dispatch' }} 51 | extra_plugins: | 52 | @semantic-release/commit-analyzer 53 | @semantic-release/release-notes-generator 54 | @semantic-release/exec 55 | @semantic-release/github 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.PAT_GLOBAL }} 58 | CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} -------------------------------------------------------------------------------- /tests/macro_def/secondary_key_mix.rs: -------------------------------------------------------------------------------- 1 | use native_db::db_type::ToInput; 2 | use native_db::db_type::{KeyDefinition, KeyEntry}; 3 | use native_db::*; 4 | use native_model::{native_model, Model}; 5 | use serde::{Deserialize, Serialize}; 6 | use std::collections::HashMap; 7 | use std::vec; 8 | 9 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] 10 | #[native_model(id = 1, version = 1)] 11 | #[native_db( 12 | primary_key(compute_primary_key -> String), 13 | secondary_key(compute_secondary_key -> String), 14 | )] 15 | struct ItemSecondaryMix { 16 | id: u32, 17 | #[secondary_key(unique)] 18 | name: String, 19 | } 20 | 21 | impl ItemSecondaryMix { 22 | pub fn compute_primary_key(&self) -> String { 23 | format!("{}-{}", self.id, self.name) 24 | } 25 | pub fn compute_secondary_key(&self) -> String { 26 | format!("{}-{}", self.name, self.id) 27 | } 28 | } 29 | 30 | #[test] 31 | fn test_secondary() { 32 | let item = ItemSecondaryMix { 33 | id: 1, 34 | name: "test".to_string(), 35 | }; 36 | 37 | let primary_key = item.native_db_primary_key(); 38 | assert_eq!(primary_key, "1-test".to_key()); 39 | 40 | let secondary_key: HashMap<_, KeyEntry> = item.native_db_secondary_keys(); 41 | assert_eq!(secondary_key.len(), 2); 42 | assert_eq!( 43 | secondary_key.get(&KeyDefinition::new( 44 | 1, 45 | 1, 46 | "compute_secondary_key", 47 | vec!["String".to_string()], 48 | Default::default() 49 | )), 50 | Some(&KeyEntry::Default("test-1".to_key())) 51 | ); 52 | 53 | assert_eq!( 54 | secondary_key 55 | .get(&KeyDefinition::new( 56 | 1, 57 | 1, 58 | "name", 59 | vec!["String".to_string()], 60 | Default::default() 61 | )) 62 | .unwrap(), 63 | &KeyEntry::Default("test".to_key()) 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /tests/query/insert_update_pk.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | 6 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 7 | #[native_model(id = 1, version = 1)] 8 | #[native_db] 9 | struct Item { 10 | #[primary_key] 11 | id: u32, 12 | name: String, 13 | } 14 | 15 | #[test] 16 | fn insert_update_pk() { 17 | let tf = TmpFs::new().unwrap(); 18 | 19 | let item = Item { 20 | id: 1, 21 | name: "test".to_string(), 22 | }; 23 | 24 | let mut models = Models::new(); 25 | models.define::().unwrap(); 26 | let db = Builder::new() 27 | .create(&models, tf.path("test").as_std_path()) 28 | .unwrap(); 29 | 30 | // Insert the item 31 | let rw = db.rw_transaction().unwrap(); 32 | rw.insert(item.clone()).unwrap(); 33 | rw.commit().unwrap(); 34 | 35 | // Check if the item is in the database 36 | let txn = db.r_transaction().unwrap(); 37 | let item2: Item = txn.get().primary(1u32).unwrap().unwrap(); 38 | assert_eq!(item, item2); 39 | 40 | let item2 = Item { 41 | id: 2, 42 | name: "test2".to_string(), 43 | }; 44 | 45 | // Update the item 46 | let rw = db.rw_transaction().unwrap(); 47 | rw.update(item.clone(), item2.clone()).unwrap(); 48 | rw.commit().unwrap(); 49 | 50 | // Check if the item v1 is not in the database 51 | let r = db.r_transaction().unwrap(); 52 | let item2: Option = r.get().primary(1u32).unwrap(); 53 | assert_eq!(item2, None); 54 | 55 | // Check if the item v2 is in the database 56 | let r = db.r_transaction().unwrap(); 57 | let item2: Item = r.get().primary(2u32).unwrap().unwrap(); 58 | assert_eq!(item2, item2); 59 | 60 | // Check if length is 1 61 | let r = db.r_transaction().unwrap(); 62 | let length = r.len().primary::().unwrap(); 63 | assert_eq!(length, 1); 64 | } 65 | -------------------------------------------------------------------------------- /tests/test_upgrade_error.rs: -------------------------------------------------------------------------------- 1 | use native_db::db_type::Error; 2 | use native_db::upgrade::UpgradeResultExt; 3 | use std::io; 4 | 5 | #[test] 6 | fn test_upgrade_context_conversion() { 7 | // Test basic error conversion 8 | let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found"); 9 | let result: Result<(), io::Error> = Err(io_error); 10 | 11 | let converted = result.upgrade_context("opening database"); 12 | assert!(converted.is_err()); 13 | 14 | match converted.unwrap_err() { 15 | Error::UpgradeMigration { context, source } => { 16 | assert_eq!(context, "opening database"); 17 | assert_eq!(source.to_string(), "file not found"); 18 | } 19 | _ => panic!("Expected UpgradeMigration error"), 20 | } 21 | } 22 | 23 | #[test] 24 | fn test_upgrade_with_item_conversion() { 25 | #[derive(Debug)] 26 | struct TestItem { 27 | id: u32, 28 | name: String, 29 | } 30 | 31 | let item = TestItem { 32 | id: 42, 33 | name: "test".to_string(), 34 | }; 35 | 36 | let io_error = io::Error::new(io::ErrorKind::PermissionDenied, "access denied"); 37 | let result: Result<(), io::Error> = Err(io_error); 38 | 39 | let converted = result.upgrade_with_item("processing", &item); 40 | assert!(converted.is_err()); 41 | 42 | match converted.unwrap_err() { 43 | Error::UpgradeMigration { context, source } => { 44 | assert!(context.contains("processing item:")); 45 | assert!(context.contains("TestItem")); 46 | assert!(context.contains("id: 42")); 47 | assert!(context.contains("name: \"test\"")); 48 | assert_eq!(source.to_string(), "access denied"); 49 | } 50 | _ => panic!("Expected UpgradeMigration error"), 51 | } 52 | } 53 | 54 | #[test] 55 | fn test_upgrade_context_preserves_ok_values() { 56 | let result: Result = Ok(42); 57 | let converted = result.upgrade_context("some operation"); 58 | assert!(converted.is_ok()); 59 | assert_eq!(converted.unwrap(), 42); 60 | } 61 | -------------------------------------------------------------------------------- /src/metadata/table.rs: -------------------------------------------------------------------------------- 1 | use crate::db_type::Result; 2 | use redb::{ReadableDatabase, TableDefinition}; 3 | 4 | pub const VERSION_NATIVE_DB_NAME: &str = "version_native_db"; 5 | pub const VERSION_NATIVE_MODEL_NAME: &str = "version_native_model"; 6 | 7 | use crate::database_instance::DatabaseInstance; 8 | 9 | const TABLE: TableDefinition<&str, &str> = TableDefinition::new("metadata"); 10 | 11 | pub fn save_metadata( 12 | database_instance: &DatabaseInstance, 13 | configuration: &super::Metadata, 14 | ) -> Result<()> { 15 | let table = database_instance.redb_database()?; 16 | let write_thx = table.begin_write()?; 17 | { 18 | let mut table = write_thx.open_table(TABLE)?; 19 | table.insert(VERSION_NATIVE_DB_NAME, configuration.current_version())?; 20 | table.insert( 21 | VERSION_NATIVE_MODEL_NAME, 22 | configuration.current_native_model_version(), 23 | )?; 24 | } 25 | write_thx.commit()?; 26 | 27 | Ok(()) 28 | } 29 | 30 | pub fn load_or_create_metadata(database_instance: &DatabaseInstance) -> Result { 31 | let database = database_instance.redb_database()?; 32 | let read_thx = database.begin_read()?; 33 | 34 | if let Ok(table) = read_thx.open_table(TABLE) { 35 | let current_version = table 36 | .get(VERSION_NATIVE_DB_NAME)? 37 | .expect("Fatal error: current_version not found"); 38 | let current_native_model_version = table 39 | .get(VERSION_NATIVE_MODEL_NAME)? 40 | .expect("Fatal error: current_native_model_version not found"); 41 | // Create metadata with the loaded versions as current versions 42 | // This preserves the actual database versions for comparison 43 | Ok(super::Metadata::from_stored( 44 | current_version.value().to_string(), 45 | current_native_model_version.value().to_string(), 46 | )) 47 | } else { 48 | // Create the metadata table if it does not exist 49 | let metadata = super::Metadata::default(); 50 | save_metadata(database_instance, &metadata)?; 51 | Ok(metadata) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/watch/event.rs: -------------------------------------------------------------------------------- 1 | use crate::db_type::{Output, Result, ToInput}; 2 | use std::fmt::Debug; 3 | 4 | #[derive(Clone)] 5 | pub enum Event { 6 | Insert(Insert), 7 | Update(Update), 8 | Delete(Delete), 9 | } 10 | 11 | impl Event { 12 | pub(crate) fn new_insert(value: Output) -> Self { 13 | Self::Insert(Insert(value)) 14 | } 15 | 16 | pub(crate) fn new_update(old_value: Output, new_value: Output) -> Self { 17 | Self::Update(Update { 18 | old: old_value, 19 | new: new_value, 20 | }) 21 | } 22 | 23 | pub(crate) fn new_delete(value: Output) -> Self { 24 | Self::Delete(Delete(value)) 25 | } 26 | } 27 | 28 | /// Get the inner value of the event 29 | /// 30 | /// NOTE: for update, it returns the new value 31 | impl Event { 32 | pub fn inner(&self) -> Result { 33 | match self { 34 | Event::Insert(insert) => insert.inner(), 35 | Event::Update(update) => update.inner_new(), 36 | Event::Delete(delete) => delete.inner(), 37 | } 38 | } 39 | } 40 | 41 | impl Debug for Event { 42 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 43 | match self { 44 | Event::Insert(_) => write!(f, "Insert"), 45 | Event::Update(_) => write!(f, "Update"), 46 | Event::Delete(_) => write!(f, "Delete"), 47 | } 48 | } 49 | } 50 | 51 | #[derive(Clone)] 52 | pub struct Insert(pub(crate) Output); 53 | 54 | impl Insert { 55 | pub fn inner(&self) -> Result { 56 | self.0.inner() 57 | } 58 | } 59 | 60 | #[derive(Clone)] 61 | pub struct Update { 62 | pub(crate) old: Output, 63 | pub(crate) new: Output, 64 | } 65 | 66 | impl Update { 67 | pub fn inner_old(&self) -> Result { 68 | self.old.inner() 69 | } 70 | pub fn inner_new(&self) -> Result { 71 | self.new.inner() 72 | } 73 | } 74 | 75 | #[derive(Clone)] 76 | pub struct Delete(pub(crate) Output); 77 | 78 | impl Delete { 79 | pub fn inner(&self) -> Result { 80 | self.0.inner() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/simple_multithreads.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | use std::sync::Arc; 6 | use std::thread; 7 | 8 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] 9 | #[native_model(id = 1, version = 1)] 10 | #[native_db] 11 | struct Item { 12 | #[primary_key] 13 | id: u32, 14 | name: String, 15 | } 16 | 17 | #[test] 18 | fn multi_threads() { 19 | let tf = TmpFs::new().unwrap(); 20 | 21 | let mut models = Models::new(); 22 | models.define::().unwrap(); 23 | let db = Builder::new() 24 | .create(&models, tf.path("test").as_std_path()) 25 | .unwrap(); 26 | 27 | let db = Arc::new(db); 28 | 29 | thread::scope(|s| { 30 | let db_thread_1 = db.clone(); 31 | let handle_thread_1 = s.spawn(move || { 32 | let item_a = Item { 33 | id: 1, 34 | name: "a".to_string(), 35 | }; 36 | { 37 | let rw = db_thread_1.rw_transaction().unwrap(); 38 | rw.insert(item_a).unwrap(); 39 | rw.commit().unwrap(); 40 | } 41 | }); 42 | 43 | let db_thread_2 = db.clone(); 44 | let handle_thread_2 = s.spawn(move || { 45 | let item_b = Item { 46 | id: 2, 47 | name: "b".to_string(), 48 | }; 49 | { 50 | let rw = db_thread_2.rw_transaction().unwrap(); 51 | rw.insert(item_b).unwrap(); 52 | rw.commit().unwrap(); 53 | } 54 | }); 55 | 56 | handle_thread_1.join().unwrap(); 57 | handle_thread_2.join().unwrap(); 58 | }); 59 | 60 | { 61 | let r = db.r_transaction().unwrap(); 62 | let len = r.len().primary::().unwrap(); 63 | assert_eq!(len, 2); 64 | 65 | let item_a: Item = r.get().primary(1u32).unwrap().unwrap(); 66 | assert_eq!(item_a.name, "a".to_string()); 67 | 68 | let item_b: Item = r.get().primary(2u32).unwrap().unwrap(); 69 | assert_eq!(item_b.name, "b".to_string()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/db_type/upgrade_required_error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use thiserror::Error; 3 | 4 | #[derive(Debug, Error)] 5 | pub struct UpgradeRequiredError { 6 | pub details: Vec, 7 | pub native_db_version: Option<(String, String)>, // (current, required) 8 | pub native_model_version: Option<(String, String)>, // (current, required) 9 | pub redb_version: Option, 10 | } 11 | 12 | impl fmt::Display for UpgradeRequiredError { 13 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 14 | write!(f, "Database upgrade required:")?; 15 | for detail in &self.details { 16 | write!(f, "\n{detail}")?; 17 | } 18 | Ok(()) 19 | } 20 | } 21 | 22 | impl Default for UpgradeRequiredError { 23 | fn default() -> Self { 24 | Self::new() 25 | } 26 | } 27 | 28 | impl UpgradeRequiredError { 29 | pub fn new() -> Self { 30 | Self { 31 | details: Vec::new(), 32 | native_db_version: None, 33 | native_model_version: None, 34 | redb_version: None, 35 | } 36 | } 37 | 38 | pub fn with_native_db_version(mut self, current: String, required: String) -> Self { 39 | self.native_db_version = Some((current.clone(), required.clone())); 40 | self.details 41 | .push(format!(" - Native DB: {current} → {required}")); 42 | self 43 | } 44 | 45 | pub fn with_native_model_version(mut self, current: String, required: String) -> Self { 46 | self.native_model_version = Some((current.clone(), required.clone())); 47 | self.details 48 | .push(format!(" - Native Model: {current} → {required}")); 49 | self 50 | } 51 | 52 | pub fn with_redb_version(mut self, version: u8) -> Self { 53 | self.redb_version = Some(version); 54 | self.details 55 | .push(format!(" - redb format: v{version} → v2")); 56 | self 57 | } 58 | 59 | pub fn is_empty(&self) -> bool { 60 | self.details.is_empty() 61 | } 62 | 63 | pub fn build(self) -> Result<(), Box> { 64 | if self.is_empty() { 65 | Ok(()) 66 | } else { 67 | Err(Box::new(self)) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/major_upgrade/tests/test_main_old.rs: -------------------------------------------------------------------------------- 1 | use major_upgrade::main_old::main_old; 2 | 3 | #[test] 4 | fn test_main_old_creates_v08x_database() { 5 | // Define the database path 6 | let db_path = std::env::temp_dir().join("native_db_v08x_example.db"); 7 | 8 | // Run the main_old function with the path 9 | let result = main_old(&db_path); 10 | 11 | // Verify it completed successfully 12 | assert!(result.is_ok(), "main_old should complete without errors"); 13 | 14 | // Verify the database was created 15 | assert!(db_path.exists(), "Database file should exist"); 16 | 17 | // Clean up 18 | std::fs::remove_file(&db_path).ok(); 19 | } 20 | 21 | #[test] 22 | fn test_v08x_database_operations() { 23 | use major_upgrade::models::v08x::V08xModel; 24 | use native_db_v0_8_x::{Builder, Models}; 25 | 26 | // Create a test-specific database file path 27 | let db_path = std::env::temp_dir().join("native_db_v08x_test.db"); 28 | 29 | // Clean up if exists 30 | if db_path.exists() { 31 | std::fs::remove_file(&db_path).unwrap(); 32 | } 33 | 34 | // Define the model 35 | let mut models = Models::new(); 36 | models.define::().unwrap(); 37 | 38 | // Create database - path is a file path 39 | let db = Builder::new().create(&models, &db_path).unwrap(); 40 | 41 | // Test insert 42 | let rw = db.rw_transaction().unwrap(); 43 | rw.insert(V08xModel { 44 | id: 100, 45 | name: "Test Model".to_string(), 46 | }) 47 | .unwrap(); 48 | rw.commit().unwrap(); 49 | 50 | // Test read 51 | let r = db.r_transaction().unwrap(); 52 | let item: V08xModel = r.get().primary(100u32).unwrap().unwrap(); 53 | assert_eq!(item.id, 100); 54 | assert_eq!(item.name, "Test Model"); 55 | 56 | // Test update 57 | let rw = db.rw_transaction().unwrap(); 58 | let mut updated_item = item.clone(); 59 | updated_item.name = "Updated Model".to_string(); 60 | rw.update(item, updated_item).unwrap(); 61 | rw.commit().unwrap(); 62 | 63 | // Verify update 64 | let r = db.r_transaction().unwrap(); 65 | let item: V08xModel = r.get().primary(100u32).unwrap().unwrap(); 66 | assert_eq!(item.name, "Updated Model"); 67 | 68 | // Clean up 69 | drop(db); 70 | std::fs::remove_file(&db_path).unwrap(); 71 | } 72 | -------------------------------------------------------------------------------- /src/watch/mod.rs: -------------------------------------------------------------------------------- 1 | mod batch; 2 | mod event; 3 | mod filter; 4 | pub mod query; 5 | mod request; 6 | mod sender; 7 | 8 | pub(crate) use batch::*; 9 | pub use event::*; 10 | pub(crate) use filter::*; 11 | pub(crate) use request::*; 12 | pub(crate) use sender::*; 13 | 14 | use std::{ 15 | sync::{Arc, RwLock}, 16 | vec, 17 | }; 18 | 19 | #[cfg(not(feature = "tokio"))] 20 | use std::sync::mpsc::SendError; 21 | #[cfg(feature = "tokio")] 22 | use tokio::sync::mpsc::error::SendError; 23 | 24 | use thiserror::Error; 25 | 26 | #[derive(Error, Debug)] 27 | pub enum WatchEventError { 28 | #[error("LockErrorPoisoned")] 29 | LockErrorPoisoned, 30 | #[cfg(not(feature = "tokio"))] 31 | #[error("SendError")] 32 | SendError(#[from] std::sync::mpsc::SendError), 33 | #[cfg(feature = "tokio")] 34 | #[error("SendError")] 35 | SendError(#[from] tokio::sync::mpsc::error::SendError), 36 | } 37 | 38 | #[cfg(not(feature = "tokio"))] 39 | pub type MpscSender = std::sync::mpsc::Sender; 40 | #[cfg(not(feature = "tokio"))] 41 | pub type MpscReceiver = std::sync::mpsc::Receiver; 42 | 43 | #[cfg(feature = "tokio")] 44 | pub type MpscSender = tokio::sync::mpsc::UnboundedSender; 45 | #[cfg(feature = "tokio")] 46 | pub type MpscReceiver = tokio::sync::mpsc::UnboundedReceiver; 47 | 48 | pub(crate) fn push_batch( 49 | senders: Arc>, 50 | batch: Batch, 51 | ) -> Result<(), WatchEventError> { 52 | let watchers = senders 53 | .read() 54 | .map_err(|_| WatchEventError::LockErrorPoisoned)?; 55 | 56 | let mut unused_watchers = vec![]; 57 | for (watcher_request, event) in batch { 58 | for (id, sender) in watchers.find_senders(&watcher_request) { 59 | let l_sender = sender.lock().unwrap(); 60 | if let Err(SendError(_)) = l_sender.send(event.clone()) { 61 | unused_watchers.push(id); 62 | } 63 | } 64 | } 65 | // Drop the lock before removing the watchers to avoid deadlock 66 | drop(watchers); 67 | 68 | // Remove unused watchers 69 | if !unused_watchers.is_empty() { 70 | let mut w = senders 71 | .write() 72 | .map_err(|_| WatchEventError::LockErrorPoisoned)?; 73 | for id in unused_watchers { 74 | w.remove_sender(id); 75 | } 76 | } 77 | 78 | Ok(()) 79 | } 80 | -------------------------------------------------------------------------------- /src/watch/filter.rs: -------------------------------------------------------------------------------- 1 | use crate::db_type::{Key, KeyDefinition, KeyOptions, KeyRange, ToKeyDefinition}; 2 | 3 | #[derive(Eq, PartialEq, Clone)] 4 | pub(crate) struct TableFilter { 5 | pub(crate) table_name: String, 6 | pub(crate) key_filter: KeyFilter, 7 | } 8 | 9 | #[derive(Eq, PartialEq, Clone)] 10 | pub(crate) enum KeyFilter { 11 | Primary(Option), 12 | PrimaryStartWith(Key), 13 | PrimaryRange(KeyRange), 14 | Secondary(KeyDefinition, Option), 15 | SecondaryStartWith(KeyDefinition, Key), 16 | SecondaryRange(KeyDefinition, KeyRange), 17 | } 18 | 19 | impl TableFilter { 20 | pub(crate) fn new_primary(table_name: String, key: Option) -> Self { 21 | Self { 22 | table_name, 23 | key_filter: KeyFilter::Primary(key.map(|k| k.to_owned())), 24 | } 25 | } 26 | 27 | pub(crate) fn new_primary_start_with(table_name: String, key_prefix: Key) -> Self { 28 | Self { 29 | table_name, 30 | key_filter: KeyFilter::PrimaryStartWith(key_prefix.to_owned()), 31 | } 32 | } 33 | 34 | pub(crate) fn new_primary_range(table_name: String, range: KeyRange) -> TableFilter { 35 | Self { 36 | table_name, 37 | key_filter: KeyFilter::PrimaryRange(range), 38 | } 39 | } 40 | 41 | pub(crate) fn new_secondary>( 42 | table_name: String, 43 | key_def: &K, 44 | key: Option, 45 | ) -> Self { 46 | Self { 47 | table_name, 48 | key_filter: KeyFilter::Secondary(key_def.key_definition(), key.map(|k| k.to_owned())), 49 | } 50 | } 51 | 52 | pub(crate) fn new_secondary_start_with>( 53 | table_name: String, 54 | key: &K, 55 | key_prefix: Key, 56 | ) -> Self { 57 | Self { 58 | table_name, 59 | key_filter: KeyFilter::SecondaryStartWith(key.key_definition(), key_prefix.to_owned()), 60 | } 61 | } 62 | 63 | pub(crate) fn new_secondary_range>( 64 | table_name: String, 65 | key_def: &K, 66 | range: KeyRange, 67 | ) -> TableFilter { 68 | Self { 69 | table_name, 70 | key_filter: KeyFilter::SecondaryRange(key_def.key_definition(), range), 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tables.toml: -------------------------------------------------------------------------------- 1 | [top_comments] 2 | Overview = """ 3 | 4 | - :warning: This benchmark is an initial version and it can certainly be greatly improved to make the results as relevant as possible. Feel free to open issues to improve it. 5 | - :point_right: Native DB will be further improved in the future as performance issues have not yet been addressed. That is indeed the purpose of this benchmark, which is to provide visibility on what needs to be improved. 6 | 7 | Comparison between [`Native DB`](https://github.com/vincent-herlemont/native_db) vs [`Redb`](https://github.com/cberner/redb) vs [`SQLite`](https://www.sqlite.org/) 8 | 9 | - Why compare with `Redb`? 10 | - To highlight the `Native DB` overhead, because `Redb` is the backend of `Native DB`, it should "normally" always be faster than `Native DB`. 11 | - Why compare with `SQLite`? 12 | - Because even though `SQLite` offers a lot more options, `Native DB` can be seen as a very light alternative to `SQLite`. 13 | - And the other databases? 14 | - Knowing the capabilities of `Native DB` compared to `Redb` with the benchmark below, you can check the benchmark of redb here: [cberner/redb/benchmarks](https://github.com/cberner/redb?tab=readme-ov-file#benchmarks) 15 | 16 | The benchmarks ignore: 17 | - [`native_model`](https://github.com/vincent-herlemont/native_model) overhead. 18 | - Serialization overhead used by `native_model` like `bincode`,`postcard` etc. 19 | - The fact that `redb` can write the data using zero-copy. 20 | 21 | Explanation: 22 | - `1:SK`, `10:SK`, `50:SK`, `100:SK`, `N:SK` in this case `N` is the number of secondary keys (`SK`) for the same data. Regarding SQLite, it is the column with each having a secondary index. 23 | - `1:T`, `n:T` represent the number of operations per transaction. 24 | - `1:T` means one operation per transaction, for example, for insertion, it means there is only one insert operation per transaction. 25 | - `n:T` means `n` operations per transaction, `n` is defined by `criterion`, meaning that all operations are within a single transaction. 26 | - We can see that `Redb` sometimes has no comparisons (`N/A`) because `Redb` is a key-value database and does not support secondary indexes. Therefore, it is pointless to compare with more or fewer secondary indexes. 27 | """ 28 | 29 | [table_comments] 30 | delete = """ 31 | :warning: We can see that when all operations are in a single transaction (`n:T`), Native DB has a huge overhead. An issue has been created to resolve this problem [#256](https://github.com/vincent-herlemont/native_db/issues/256). 32 | """ -------------------------------------------------------------------------------- /src/transaction/internal/r_transaction.rs: -------------------------------------------------------------------------------- 1 | use crate::db_type::{Error, Key, KeyDefinition, KeyOptions, Result}; 2 | use crate::table_definition::PrimaryTableDefinition; 3 | use crate::transaction::internal::private_readable_transaction::PrivateReadableTransaction; 4 | use crate::Model; 5 | use std::collections::HashMap; 6 | 7 | pub struct InternalRTransaction<'db> { 8 | pub(crate) redb_transaction: redb::ReadTransaction, 9 | pub(crate) table_definitions: &'db HashMap>, 10 | } 11 | 12 | impl<'db, 'txn> PrivateReadableTransaction<'db, 'txn> for InternalRTransaction<'db> 13 | where 14 | Self: 'txn, 15 | Self: 'db, 16 | { 17 | type RedbPrimaryTable = redb::ReadOnlyTable; 18 | type RedbSecondaryTable = redb::ReadOnlyMultimapTable; 19 | 20 | type RedbTransaction<'db_bis> 21 | = redb::ReadTransaction 22 | where 23 | Self: 'db_bis; 24 | 25 | fn table_definitions(&self) -> &HashMap> { 26 | self.table_definitions 27 | } 28 | 29 | fn get_primary_table(&'txn self, model: &Model) -> Result { 30 | let table_definition = self 31 | .table_definitions() 32 | .get(model.primary_key.unique_table_name.as_str()) 33 | .ok_or_else(|| Error::TableDefinitionNotFound { 34 | table: model.primary_key.unique_table_name.to_string(), 35 | })?; 36 | let table = self.redb_transaction.open_table(table_definition.redb)?; 37 | Ok(table) 38 | } 39 | 40 | fn get_secondary_table( 41 | &'txn self, 42 | model: &Model, 43 | secondary_key: &KeyDefinition, 44 | ) -> Result { 45 | let main_table_definition = self 46 | .table_definitions() 47 | .get(model.primary_key.unique_table_name.as_str()) 48 | .ok_or_else(|| Error::TableDefinitionNotFound { 49 | table: model.primary_key.unique_table_name.to_string(), 50 | })?; 51 | let secondary_table_definition = main_table_definition 52 | .secondary_tables 53 | .get(secondary_key) 54 | .ok_or_else(|| Error::TableDefinitionNotFound { 55 | table: secondary_key.unique_table_name.to_string(), 56 | })?; 57 | let table = self 58 | .redb_transaction 59 | .open_multimap_table(secondary_table_definition.redb)?; 60 | Ok(table) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/table_definition.rs: -------------------------------------------------------------------------------- 1 | use crate::database_builder::ModelBuilder; 2 | use crate::db_type::{Key, KeyDefinition, KeyOptions}; 3 | use std::collections::HashMap; 4 | use std::fmt::Debug; 5 | 6 | pub(crate) type RedbPrimaryTableDefinition<'a> = redb::TableDefinition<'a, Key, &'static [u8]>; 7 | pub(crate) type RedbSecondaryTableDefinition<'a> = redb::MultimapTableDefinition<'a, Key, Key>; 8 | 9 | pub struct PrimaryTableDefinition<'a> { 10 | pub(crate) model: crate::Model, 11 | pub(crate) redb: RedbPrimaryTableDefinition<'a>, 12 | pub(crate) secondary_tables: HashMap, SecondaryTableDefinition<'a>>, 13 | pub(crate) native_model_options: NativeModelOptions, 14 | } 15 | 16 | #[derive(Clone, Debug, Default)] 17 | pub struct NativeModelOptions { 18 | pub(crate) native_model_id: u32, 19 | pub(crate) native_model_version: u32, 20 | // If a model as a new version, the old version is still available but marked as legacy. 21 | // NOTE: Is impossible to write or read on a legacy table definition. 22 | // Just a migration to a new version is allowed. 23 | pub(crate) native_model_legacy: bool, 24 | } 25 | 26 | impl<'a> From<(&ModelBuilder, RedbPrimaryTableDefinition<'a>)> for PrimaryTableDefinition<'a> { 27 | fn from(input: (&ModelBuilder, RedbPrimaryTableDefinition<'a>)) -> Self { 28 | let (builder, redb) = input; 29 | Self { 30 | model: builder.model.clone(), 31 | redb, 32 | secondary_tables: HashMap::new(), 33 | native_model_options: builder.native_model_options.clone(), 34 | } 35 | } 36 | } 37 | 38 | impl Debug for PrimaryTableDefinition<'_> { 39 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 40 | use redb::TableHandle; 41 | f.debug_struct("TableDefinition") 42 | .field("name", &self.redb.name()) 43 | .field("model_id", &self.native_model_options.native_model_id) 44 | .field( 45 | "model_version", 46 | &self.native_model_options.native_model_version, 47 | ) 48 | .field("legacy", &self.native_model_options.native_model_legacy) 49 | .finish() 50 | } 51 | } 52 | 53 | #[derive(Clone)] 54 | pub(crate) struct SecondaryTableDefinition<'a> { 55 | pub(crate) redb: RedbSecondaryTableDefinition<'a>, 56 | } 57 | 58 | impl<'a> From> for SecondaryTableDefinition<'a> { 59 | fn from(rdb: RedbSecondaryTableDefinition<'a>) -> SecondaryTableDefinition<'a> { 60 | Self { redb: rdb } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/upgrade.rs: -------------------------------------------------------------------------------- 1 | use crate::db_type::Error; 2 | 3 | /// Extension trait for converting errors during database upgrade migrations. 4 | /// 5 | /// This trait provides convenient methods to convert errors from old database versions 6 | /// into the current error type with proper context. 7 | /// 8 | /// # Example 9 | /// 10 | /// ```ignore 11 | /// use native_db::upgrade::UpgradeResultExt; 12 | /// 13 | /// // Inside an upgrade closure: 14 | /// let old_db = V08xBuilder::new() 15 | /// .open(&old_models, &db_path) 16 | /// .upgrade_context("opening old database")?; 17 | /// ``` 18 | pub trait UpgradeResultExt { 19 | /// Converts an error into an `UpgradeMigration` error with the given context. 20 | /// 21 | /// # Arguments 22 | /// 23 | /// * `context` - A description of what operation was being performed when the error occurred 24 | /// 25 | /// # Example 26 | /// 27 | /// ```ignore 28 | /// old_models.define::() 29 | /// .upgrade_context("defining old model")?; 30 | /// ``` 31 | fn upgrade_context(self, context: &str) -> Result; 32 | 33 | /// Converts an error into an `UpgradeMigration` error with a context that includes 34 | /// information about a specific item being processed. 35 | /// 36 | /// # Arguments 37 | /// 38 | /// * `context` - A description of the operation 39 | /// * `item` - The item being processed (will be formatted using Debug) 40 | /// 41 | /// # Example 42 | /// 43 | /// ```ignore 44 | /// process_item(&item) 45 | /// .upgrade_with_item("processing", &item)?; 46 | /// ``` 47 | fn upgrade_with_item(self, context: &str, item: &I) -> Result; 48 | } 49 | 50 | impl UpgradeResultExt for Result 51 | where 52 | E: std::error::Error + Send + Sync + 'static, 53 | { 54 | fn upgrade_context(self, context: &str) -> Result { 55 | self.map_err(|e| Error::UpgradeMigration { 56 | context: context.to_string(), 57 | source: Box::new(e), 58 | }) 59 | } 60 | 61 | fn upgrade_with_item(self, context: &str, item: &I) -> Result { 62 | self.map_err(|e| Error::UpgradeMigration { 63 | context: format!("{context} item: {item:?}"), 64 | source: Box::new(e), 65 | }) 66 | } 67 | } 68 | 69 | /// A prelude module that re-exports commonly used upgrade-related items. 70 | /// 71 | /// # Example 72 | /// 73 | /// ```ignore 74 | /// use native_db::upgrade::prelude::*; 75 | /// ``` 76 | pub mod prelude { 77 | pub use super::UpgradeResultExt; 78 | } 79 | -------------------------------------------------------------------------------- /tests/query/insert_update_sk.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | 6 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 7 | #[native_model(id = 1, version = 1)] 8 | #[native_db] 9 | struct Item { 10 | #[primary_key] 11 | id: u32, 12 | #[secondary_key(unique)] 13 | name: String, 14 | } 15 | 16 | #[test] 17 | fn insert_update_sk() { 18 | let tf = TmpFs::new().unwrap(); 19 | 20 | let item = Item { 21 | id: 1, 22 | name: "test".to_string(), 23 | }; 24 | 25 | let mut models = Models::new(); 26 | models.define::().unwrap(); 27 | let db = Builder::new() 28 | .create(&models, tf.path("test").as_std_path()) 29 | .unwrap(); 30 | 31 | // Insert the item 32 | let rw = db.rw_transaction().unwrap(); 33 | rw.insert(item.clone()).unwrap(); 34 | rw.commit().unwrap(); 35 | 36 | // Check if the item is in the database by primary key 37 | let r = db.r_transaction().unwrap(); 38 | let item2: Item = r.get().primary(1u32).unwrap().unwrap(); 39 | assert_eq!(item, item2); 40 | 41 | // Check if the item is in the database by secondary key 42 | let r = db.r_transaction().unwrap(); 43 | let item2: Item = r.get().secondary(ItemKey::name, "test").unwrap().unwrap(); 44 | assert_eq!(item, item2); 45 | 46 | let item_v2 = Item { 47 | id: 2, 48 | name: "test2".to_string(), 49 | }; 50 | 51 | // Update the item 52 | let rw = db.rw_transaction().unwrap(); 53 | rw.update(item.clone(), item_v2.clone()).unwrap(); 54 | rw.commit().unwrap(); 55 | 56 | // Check if the item v1 is not in the database by primary key 57 | let r = db.r_transaction().unwrap(); 58 | let item2: Option = r.get().primary(1u32).unwrap(); 59 | assert_eq!(item2, None); 60 | 61 | // Check if the item v1 is not in the database by secondary key 62 | let r = db.r_transaction().unwrap(); 63 | let item2: Option = r.get().secondary(ItemKey::name, "test").unwrap(); 64 | assert_eq!(item2, None); 65 | 66 | // Check if the item v2 is in the database by primary key 67 | let r = db.r_transaction().unwrap(); 68 | let item2: Item = r.get().secondary(ItemKey::name, "test2").unwrap().unwrap(); 69 | assert_eq!(item2, item_v2); 70 | 71 | // Check if the item v2 is in the database by secondary key 72 | let r = db.r_transaction().unwrap(); 73 | let item2: Item = r.get().primary(2u32).unwrap().unwrap(); 74 | assert_eq!(item2, item_v2); 75 | 76 | // Check length is 1 77 | let r = db.r_transaction().unwrap(); 78 | let length = r.len().primary::().unwrap(); 79 | assert_eq!(length, 1); 80 | } 81 | -------------------------------------------------------------------------------- /src/transaction/internal/private_readable_transaction.rs: -------------------------------------------------------------------------------- 1 | use crate::db_type::{ 2 | Error, Key, KeyDefinition, KeyOptions, Output, Result, ToKey, ToKeyDefinition, 3 | }; 4 | use crate::table_definition::PrimaryTableDefinition; 5 | use crate::Model; 6 | use redb::ReadableTableMetadata; 7 | use redb::{ReadableMultimapTable, ReadableTable}; 8 | use std::collections::HashMap; 9 | 10 | pub trait PrivateReadableTransaction<'db, 'txn> { 11 | type RedbPrimaryTable: ReadableTable; 12 | type RedbSecondaryTable: ReadableMultimapTable; 13 | 14 | type RedbTransaction<'db_bis> 15 | where 16 | Self: 'db_bis; 17 | 18 | fn table_definitions(&self) -> &HashMap>; 19 | 20 | fn get_primary_table(&'txn self, model: &Model) -> Result; 21 | 22 | fn get_secondary_table( 23 | &'txn self, 24 | model: &Model, 25 | secondary_key: &KeyDefinition, 26 | ) -> Result; 27 | 28 | fn get_by_primary_key(&'txn self, model: Model, key: impl ToKey) -> Result> { 29 | let table = self.get_primary_table(&model)?; 30 | let key = key.to_key(); 31 | let item = table.get(key)?; 32 | Ok(item.map(|item| item.value().into())) 33 | } 34 | 35 | fn get_by_secondary_key( 36 | &'txn self, 37 | model: Model, 38 | key_def: impl ToKeyDefinition, 39 | key: impl ToKey, 40 | ) -> Result> { 41 | let secondary_key = key_def.key_definition(); 42 | // Provide a better error for the test of unicity of the secondary key 43 | model.check_secondary_options(&secondary_key, |options| options.unique)?; 44 | 45 | let table = self.get_secondary_table(&model, &secondary_key)?; 46 | 47 | let mut primary_keys = table.get(key.to_key())?; 48 | let primary_key = if let Some(primary_key) = primary_keys.next() { 49 | let primary_key = primary_key?; 50 | primary_key.value().to_owned() 51 | } else { 52 | return Ok(None); 53 | }; 54 | 55 | Ok(Some( 56 | self.get_by_primary_key(model, primary_key)? 57 | .ok_or(Error::PrimaryKeyNotFound)?, 58 | )) 59 | } 60 | 61 | fn primary_len(&'txn self, model: Model) -> Result { 62 | let table = self.get_primary_table(&model)?; 63 | let result = table.len()?; 64 | Ok(result) 65 | } 66 | 67 | fn secondary_len( 68 | &'txn self, 69 | model: Model, 70 | key_def: impl ToKeyDefinition, 71 | ) -> Result { 72 | let table = self.get_secondary_table(&model, &key_def.key_definition())?; 73 | let result = table.len()?; 74 | Ok(result) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /native_db_macro/src/native_db.rs: -------------------------------------------------------------------------------- 1 | use crate::model_attributes::ModelAttributes; 2 | use crate::model_native_db::ModelNativeDB; 3 | use crate::struct_name::StructName; 4 | use proc_macro::TokenStream; 5 | use quote::quote; 6 | use syn::{parse_macro_input, Data, DeriveInput, Fields}; 7 | 8 | pub fn native_db(args: TokenStream, input: TokenStream) -> TokenStream { 9 | let ast = parse_macro_input!(input as DeriveInput); 10 | let struct_name = StructName::new(ast.ident.clone()); 11 | 12 | let mut attrs = ModelAttributes { 13 | struct_name: struct_name.clone(), 14 | primary_key: None, 15 | secondary_keys: Default::default(), 16 | do_export_keys: None, 17 | }; 18 | let model_attributes_parser = syn::meta::parser(|meta| attrs.parse(meta)); 19 | parse_macro_input!(args with model_attributes_parser); 20 | 21 | if let Data::Struct(data_struct) = &ast.data { 22 | if let Fields::Named(fields) = &data_struct.fields { 23 | for field in &fields.named { 24 | if let Err(err) = attrs.parse_field(field) { 25 | return TokenStream::from(err.to_compile_error()); 26 | } 27 | } 28 | } 29 | } 30 | 31 | let model_native_db = ModelNativeDB::new(struct_name.clone(), attrs.clone()); 32 | 33 | let native_db_pk = model_native_db.native_db_primary_key(); 34 | let native_db_gks = model_native_db.native_db_secondary_key(); 35 | let native_db_model = model_native_db.native_db_model(); 36 | 37 | let keys_enum_visibility = model_native_db.keys_enum_visibility(); 38 | let keys_enum_name = model_native_db.keys_enum_name(); 39 | let keys_enum = model_native_db.secondary_keys_enum(); 40 | let keys_enum_database_key = model_native_db.keys_enum_database_key(); 41 | 42 | let struct_name = struct_name.ident(); 43 | let gen = quote! { 44 | #[derive(native_db::KeyAttributes)] 45 | #ast 46 | 47 | impl native_db::db_type::ToInput for #struct_name { 48 | fn native_db_bincode_encode_to_vec(&self) -> native_db::db_type::Result> { 49 | native_db::bincode_encode_to_vec(self) 50 | } 51 | 52 | fn native_db_bincode_decode_from_slice(slice: &[u8]) -> native_db::db_type::Result { 53 | Ok(native_db::bincode_decode_from_slice(slice)?.0) 54 | } 55 | 56 | #native_db_model 57 | #native_db_pk 58 | #native_db_gks 59 | } 60 | 61 | #[allow(non_camel_case_types)] 62 | #keys_enum_visibility enum #keys_enum_name { 63 | #(#keys_enum),* 64 | } 65 | 66 | impl native_db::db_type::ToKeyDefinition for #keys_enum_name { 67 | #keys_enum_database_key 68 | } 69 | }; 70 | 71 | gen.into() 72 | } 73 | -------------------------------------------------------------------------------- /src/database_instance.rs: -------------------------------------------------------------------------------- 1 | use crate::db_type::{Result, UpgradeRequiredError}; 2 | use redb::Builder; 3 | use std::path::Path; 4 | use std::path::PathBuf; 5 | 6 | pub(crate) struct DatabaseInstance { 7 | kind: DatabaseInstanceKind, 8 | } 9 | 10 | impl DatabaseInstance { 11 | pub(crate) fn create_on_disk(builder: Builder, path: impl AsRef) -> Result { 12 | let db = builder.create(path.as_ref())?; 13 | Ok(Self { 14 | kind: DatabaseInstanceKind::OnDisk { 15 | redb_database: db, 16 | path: path.as_ref().to_path_buf(), 17 | }, 18 | }) 19 | } 20 | 21 | pub(crate) fn open_on_disk(builder: Builder, path: impl AsRef) -> Result { 22 | match builder.open(path.as_ref()) { 23 | Ok(db) => Ok(Self { 24 | kind: DatabaseInstanceKind::OnDisk { 25 | redb_database: db, 26 | path: path.as_ref().to_path_buf(), 27 | }, 28 | }), 29 | Err(redb::DatabaseError::UpgradeRequired(version)) => Err(Box::new( 30 | UpgradeRequiredError::new().with_redb_version(version), 31 | ) 32 | .build() 33 | .unwrap_err() 34 | .into()), 35 | Err(e) => Err(e.into()), 36 | } 37 | } 38 | 39 | pub(crate) fn create_in_memory(builder: Builder) -> Result { 40 | let in_memory_backend = redb::backends::InMemoryBackend::new(); 41 | let db = builder.create_with_backend(in_memory_backend)?; 42 | Ok(Self { 43 | kind: DatabaseInstanceKind::InMemory { redb_database: db }, 44 | }) 45 | } 46 | 47 | pub(crate) fn redb_database(&self) -> Result<&redb::Database> { 48 | self.kind.redb_database() 49 | } 50 | 51 | pub(crate) fn redb_database_mut(&mut self) -> Result<&mut redb::Database> { 52 | self.kind.redb_database_mut() 53 | } 54 | } 55 | 56 | enum DatabaseInstanceKind { 57 | InMemory { 58 | redb_database: redb::Database, 59 | }, 60 | OnDisk { 61 | redb_database: redb::Database, 62 | #[allow(dead_code)] 63 | path: PathBuf, 64 | }, 65 | } 66 | 67 | impl DatabaseInstanceKind { 68 | pub(crate) fn redb_database(&self) -> Result<&redb::Database> { 69 | match self { 70 | DatabaseInstanceKind::InMemory { redb_database } => Ok(redb_database), 71 | DatabaseInstanceKind::OnDisk { redb_database, .. } => Ok(redb_database), 72 | } 73 | } 74 | 75 | pub(crate) fn redb_database_mut(&mut self) -> Result<&mut redb::Database> { 76 | match self { 77 | DatabaseInstanceKind::InMemory { redb_database } => Ok(redb_database), 78 | DatabaseInstanceKind::OnDisk { redb_database, .. } => Ok(redb_database), 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/convert_all.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | 6 | type Item = ItemV1; 7 | 8 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] 9 | #[native_model(id = 1, version = 1)] 10 | #[native_db] 11 | struct ItemV0 { 12 | #[primary_key] 13 | pub id: u32, 14 | } 15 | 16 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] 17 | #[native_model(id = 2, version = 1)] 18 | #[native_db] 19 | struct ItemV1 { 20 | #[primary_key] 21 | pub id: String, 22 | } 23 | 24 | impl From for ItemV1 { 25 | fn from(item: ItemV0) -> Self { 26 | ItemV1 { 27 | id: item.id.to_string(), 28 | } 29 | } 30 | } 31 | 32 | #[test] 33 | fn convert_all() { 34 | let tf = TmpFs::new().unwrap(); 35 | 36 | let mut models = Models::new(); 37 | models.define::().unwrap(); 38 | models.define::().unwrap(); 39 | let db = Builder::new() 40 | .create(&models, tf.path("test").as_std_path()) 41 | .unwrap(); 42 | 43 | let a = ItemV0 { id: 42 }; 44 | 45 | let rw_txn = db.rw_transaction().unwrap(); 46 | rw_txn.insert(a.clone()).unwrap(); 47 | rw_txn.commit().unwrap(); 48 | 49 | // Check if a is saved 50 | let txn = db.rw_transaction().unwrap(); 51 | let a1 = txn.get().primary(a.id).unwrap().unwrap(); 52 | assert_eq!(a, a1); 53 | txn.commit().unwrap(); 54 | 55 | #[allow(unused_mut)] 56 | #[cfg(not(feature = "tokio"))] 57 | let (mut recv_av1, _id) = db.watch().scan().primary().all::().unwrap(); 58 | #[allow(unused_mut)] 59 | #[cfg(not(feature = "tokio"))] 60 | let (mut recv_av2, _id) = db.watch().scan().primary().all::().unwrap(); 61 | 62 | #[cfg(feature = "tokio")] 63 | let (mut recv_av1, _id) = db.watch().scan().primary().all::().unwrap(); 64 | #[cfg(feature = "tokio")] 65 | let (mut recv_av2, _id) = db.watch().scan().primary().all::().unwrap(); 66 | 67 | // Migrate 68 | let rw_txn = db.rw_transaction().unwrap(); 69 | rw_txn.convert_all::().unwrap(); 70 | rw_txn.commit().unwrap(); 71 | 72 | // Check is there is no event from AV1 73 | assert!(recv_av1.try_recv().is_err()); 74 | // Check is there is no event from AV2 75 | assert!(recv_av2.try_recv().is_err()); 76 | 77 | // Check migration 78 | let r_txn = db.r_transaction().unwrap(); 79 | let len_av1 = r_txn.len().primary::().unwrap(); 80 | assert_eq!(len_av1, 0); 81 | let len_av2 = r_txn.len().primary::().unwrap(); 82 | assert_eq!(len_av2, 1); 83 | 84 | let a2: Item = r_txn.get().primary("42").unwrap().unwrap(); 85 | assert_eq!( 86 | a2, 87 | Item { 88 | id: "42".to_string() 89 | } 90 | ); 91 | } 92 | -------------------------------------------------------------------------------- /tests/metadata/current_version.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 6 | #[native_model(id = 1, version = 1)] 7 | #[native_db] 8 | struct Item1 { 9 | #[primary_key] 10 | id: u32, 11 | #[secondary_key(unique)] 12 | name: String, 13 | } 14 | 15 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 16 | #[native_model(id = 2, version = 1)] 17 | #[native_db] 18 | struct Item2 { 19 | #[primary_key] 20 | id: u32, 21 | #[secondary_key(optional)] 22 | id2: Option, 23 | #[secondary_key] 24 | name: String, 25 | } 26 | 27 | #[test] 28 | #[ignore = "Upgrade features have been removed"] 29 | fn test_current_version() { 30 | use std::path::PathBuf; 31 | #[cfg(any(target_os = "android", target_os = "ios"))] 32 | let database_path = { dinghy_test::test_project_path().join("tests/data/db_0_7_1") }; 33 | 34 | #[cfg(not(any(target_os = "android", target_os = "ios")))] 35 | let database_path = { 36 | let root_project_path = env!("CARGO_MANIFEST_DIR"); 37 | PathBuf::from(format!("{}/tests/data/db_0_7_1", root_project_path)) 38 | }; 39 | 40 | use shortcut_assert_fs::TmpFs; 41 | let tmp = TmpFs::new().unwrap(); 42 | 43 | // Copy the legacy database to the temporary directory. 44 | let tmp_database_path = tmp.path("db_0_7_1"); 45 | std::fs::copy(&database_path, &tmp_database_path).unwrap(); 46 | 47 | // Open the legacy database with the upgrade feature. This must succeed. 48 | let mut models = Models::new(); 49 | models.define::().unwrap(); 50 | models.define::().unwrap(); 51 | let db = Builder::new().open(&models, &tmp_database_path).unwrap(); 52 | 53 | let metadata = db.metadata(); 54 | assert_eq!(metadata.current_version(), env!("CARGO_PKG_VERSION")); 55 | assert_eq!(metadata.current_native_model_version(), "0.4.19"); 56 | 57 | // During open, the database add the metadata table 58 | assert_eq!(metadata.previous_version(), None); 59 | assert_eq!(metadata.previous_native_model_version(), None); 60 | 61 | // Verify it's a new database (no previous version) 62 | 63 | drop(db); 64 | 65 | let db = Builder::new().open(&models, &tmp_database_path).unwrap(); 66 | 67 | let metadata = db.metadata(); 68 | assert_eq!(metadata.current_version(), env!("CARGO_PKG_VERSION")); 69 | assert_eq!(metadata.current_native_model_version(), "0.4.19"); 70 | 71 | // During open, the database add the metadata table 72 | assert_eq!(metadata.previous_version(), Some(env!("CARGO_PKG_VERSION"))); 73 | assert_eq!(metadata.previous_native_model_version(), Some("0.4.19")); 74 | 75 | // Verify the database has been opened before (has previous version) 76 | } 77 | 78 | // TODO: add test for version <=0.8.0 79 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | "main" 4 | ], 5 | "tagFormat": "${version}", 6 | "plugins": [ 7 | [ 8 | "@semantic-release/commit-analyzer", 9 | { 10 | "releaseRules": [ 11 | { 12 | "breaking": true, 13 | "release": "minor" 14 | }, 15 | { 16 | "revert": true, 17 | "release": "patch" 18 | }, 19 | { 20 | "type": "feat", 21 | "release": "minor" 22 | }, 23 | { 24 | "type": "fix", 25 | "release": "patch" 26 | }, 27 | { 28 | "type": "perf", 29 | "release": "patch" 30 | }, 31 | { 32 | "type": "docs", 33 | "release": "patch" 34 | }, 35 | { 36 | "emoji": ":racehorse:", 37 | "release": "patch" 38 | }, 39 | { 40 | "emoji": ":bug:", 41 | "release": "patch" 42 | }, 43 | { 44 | "emoji": ":penguin:", 45 | "release": "patch" 46 | }, 47 | { 48 | "emoji": ":apple:", 49 | "release": "patch" 50 | }, 51 | { 52 | "emoji": ":checkered_flag:", 53 | "release": "patch" 54 | }, 55 | { 56 | "tag": "BUGFIX", 57 | "release": "patch" 58 | }, 59 | { 60 | "tag": "FEATURE", 61 | "release": "minor" 62 | }, 63 | { 64 | "tag": "SECURITY", 65 | "release": "patch" 66 | }, 67 | { 68 | "tag": "Breaking", 69 | "release": "minor" 70 | }, 71 | { 72 | "tag": "Fix", 73 | "release": "patch" 74 | }, 75 | { 76 | "tag": "Update", 77 | "release": "minor" 78 | }, 79 | { 80 | "tag": "New", 81 | "release": "minor" 82 | }, 83 | { 84 | "component": "perf", 85 | "release": "patch" 86 | }, 87 | { 88 | "component": "deps", 89 | "release": "patch" 90 | }, 91 | { 92 | "type": "FEAT", 93 | "release": "minor" 94 | }, 95 | { 96 | "type": "FIX", 97 | "release": "patch" 98 | } 99 | ] 100 | } 101 | ], 102 | "@semantic-release/release-notes-generator", 103 | [ 104 | "@semantic-release/exec", 105 | { 106 | "prepareCmd": "bash version_update.sh ${nextRelease.version}", 107 | "publishCmd": "bash cargo_publish.sh" 108 | } 109 | ], 110 | "@semantic-release/github" 111 | ] 112 | } -------------------------------------------------------------------------------- /tests/check_type/struct_custom.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] 6 | #[native_model(id = 1, version = 1)] 7 | #[native_db( 8 | primary_key(custom_id -> u32), 9 | // secondary_key(custom_sk -> Vec), 10 | // secondary_key(custom_sk_o -> Option>, optional), 11 | // secondary_key(custom_sk_u -> Vec, unique), 12 | // secondary_key(custom_sk_o_u -> Option>, unique, optional), 13 | secondary_key(custom_sk_no_u -> Option>, unique), 14 | )] 15 | struct ItemCustomPk { 16 | id: u32, 17 | all_sk: Vec, 18 | } 19 | 20 | impl ItemCustomPk { 21 | fn custom_id(&self) -> u32 { 22 | self.id 23 | } 24 | 25 | // fn custom_sk(&self) -> Vec { 26 | // self.all_sk.clone() 27 | // } 28 | 29 | // fn custom_sk_u(&self) -> Vec { 30 | // self.all_sk.clone() 31 | // } 32 | 33 | // fn custom_sk_o(&self) -> Option> { 34 | // Some(self.all_sk.clone()) 35 | // } 36 | 37 | // fn custom_sk_o_u(&self) -> Option> { 38 | // Some(self.all_sk.clone()) 39 | // } 40 | 41 | fn custom_sk_no_u(&self) -> Option> { 42 | Some(self.all_sk.clone()) 43 | } 44 | } 45 | 46 | #[test] 47 | fn test_get_primary_key_custom_pk() { 48 | let item = ItemCustomPk { 49 | id: 1, 50 | all_sk: vec!["1".to_string()], 51 | }; 52 | 53 | let mut models = Models::new(); 54 | models.define::().unwrap(); 55 | let db = Builder::new().create_in_memory(&models).unwrap(); 56 | 57 | let rw = db.rw_transaction().unwrap(); 58 | rw.insert(item.clone()).unwrap(); 59 | rw.commit().unwrap(); 60 | 61 | // // Get primary key for read transaction for unique 62 | // let r = db.r_transaction().unwrap(); 63 | // let id = vec!["1".to_string()]; 64 | // let result_item = r 65 | // .get() 66 | // .secondary(ItemCustomPkKey::custom_sk_u, id) 67 | // .unwrap() 68 | // .unwrap(); 69 | 70 | // assert_eq!(item, result_item); 71 | 72 | // // Get primary key for read transaction for unique optional 73 | // let r = db.r_transaction().unwrap(); 74 | // let id = vec!["1".to_string()]; 75 | // let result_item = r 76 | // .get() 77 | // .secondary(ItemCustomPkKey::custom_sk_o_u, id) 78 | // .unwrap() 79 | // .unwrap(); 80 | 81 | // assert_eq!(item, result_item); 82 | 83 | // Get primary key for read transaction for unique not optional 84 | let r = db.r_transaction().unwrap(); 85 | let id = Some(vec!["1".to_string()]); 86 | let result_item = r 87 | .get() 88 | .secondary(ItemCustomPkKey::custom_sk_no_u, id) 89 | .unwrap() 90 | .unwrap(); 91 | 92 | assert_eq!(item, result_item); 93 | } 94 | -------------------------------------------------------------------------------- /src/watch/query/get.rs: -------------------------------------------------------------------------------- 1 | use crate::db_type::{ 2 | check_key_type, check_key_type_from_key_definition, KeyOptions, Result, ToInput, ToKey, 3 | ToKeyDefinition, 4 | }; 5 | use crate::watch; 6 | use crate::watch::query::internal; 7 | use crate::watch::MpscReceiver; 8 | 9 | /// Watch only one value. 10 | pub struct WatchGet<'db, 'w> { 11 | pub(crate) internal: &'w internal::InternalWatch<'db>, 12 | } 13 | 14 | impl WatchGet<'_, '_> { 15 | /// Watch the primary key. 16 | /// 17 | /// Returns a channel receiver and the watcher id. 18 | /// The watcher id can be used to unwatch the channel. 19 | /// 20 | /// # Example 21 | /// ```rust 22 | /// use native_db::*; 23 | /// use native_db::native_model::{native_model, Model}; 24 | /// use serde::{Deserialize, Serialize}; 25 | /// 26 | /// #[derive(Serialize, Deserialize)] 27 | /// #[native_model(id=1, version=1)] 28 | /// #[native_db] 29 | /// struct Data { 30 | /// #[primary_key] 31 | /// id: u64, 32 | /// } 33 | /// 34 | /// fn main() -> Result<(), db_type::Error> { 35 | /// let mut models = Models::new(); 36 | /// models.define::()?; 37 | /// let db = Builder::new().create_in_memory(&models)?; 38 | /// 39 | /// // Watch the primary key 40 | /// let (_recv, _id) = db.watch().get().primary::(1u64)?; 41 | /// Ok(()) 42 | /// } 43 | /// ``` 44 | pub fn primary( 45 | &self, 46 | key: impl ToKey, 47 | ) -> Result<(MpscReceiver, u64)> { 48 | let model = T::native_db_model(); 49 | check_key_type(&model, &key)?; 50 | self.internal.watch_primary::(key) 51 | } 52 | 53 | /// Watch the secondary key. 54 | /// 55 | /// Returns a channel receiver and the watcher id. 56 | /// The watcher id can be used to unwatch the channel. 57 | /// 58 | /// # Example 59 | /// ```rust 60 | /// use native_db::*; 61 | /// use native_db::native_model::{native_model, Model}; 62 | /// use serde::{Deserialize, Serialize}; 63 | /// 64 | /// #[derive(Serialize, Deserialize)] 65 | /// #[native_model(id=1, version=1)] 66 | /// #[native_db] 67 | /// struct Data { 68 | /// #[primary_key] 69 | /// id: u64, 70 | /// #[secondary_key] 71 | /// name: String, 72 | /// } 73 | /// 74 | /// fn main() -> Result<(), db_type::Error> { 75 | /// let mut models = Models::new(); 76 | /// models.define::()?; 77 | /// let db = Builder::new().create_in_memory(&models)?; 78 | /// 79 | /// // Watch the secondary key name 80 | /// let (_recv, _id) = db.watch().get().secondary::(DataKey::name, "test")?; 81 | /// Ok(()) 82 | /// } 83 | /// ``` 84 | pub fn secondary( 85 | &self, 86 | key_def: impl ToKeyDefinition, 87 | key: impl ToKey, 88 | ) -> Result<(MpscReceiver, u64)> { 89 | check_key_type_from_key_definition(&key_def.key_definition(), &key)?; 90 | self.internal.watch_secondary::(&key_def, key) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/primary_drain/with_secondary_keys.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | 6 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] 7 | #[native_model(id = 1, version = 1)] 8 | #[native_db( 9 | primary_key(generate_my_primary_key -> u32), 10 | secondary_key(generate_my_secondary_key -> String, unique) 11 | )] 12 | struct Item { 13 | id: u32, 14 | name: String, 15 | tag: String, 16 | } 17 | 18 | impl Item { 19 | pub fn generate_my_primary_key(&self) -> u32 { 20 | self.id 21 | } 22 | 23 | pub fn generate_my_secondary_key(&self) -> String { 24 | let mut tag = self.tag.clone(); 25 | let primary_key = self.generate_my_primary_key().to_string(); 26 | tag.push_str(&primary_key); 27 | tag 28 | } 29 | 30 | pub fn inc(&mut self) -> &Self { 31 | self.id += 1; 32 | self 33 | } 34 | } 35 | 36 | #[test] 37 | fn drain_all() { 38 | let tf = TmpFs::new().unwrap(); 39 | 40 | let mut item = Item { 41 | id: 1, 42 | name: "test".to_string(), 43 | tag: "red".to_string(), 44 | }; 45 | 46 | let mut models = Models::new(); 47 | models.define::().unwrap(); 48 | let db = Builder::new() 49 | .create(&models, tf.path("test").as_std_path()) 50 | .unwrap(); 51 | 52 | // Insert 5 items 53 | let rw = db.rw_transaction().unwrap(); 54 | rw.insert(item.clone()).unwrap(); 55 | rw.insert(item.inc().clone()).unwrap(); 56 | rw.insert(item.inc().clone()).unwrap(); 57 | rw.insert(item.inc().clone()).unwrap(); 58 | rw.insert(item.inc().clone()).unwrap(); 59 | rw.commit().unwrap(); 60 | 61 | let stats = db.redb_stats().unwrap(); 62 | assert_eq!(stats.primary_tables.len(), 1); 63 | assert_eq!(stats.primary_tables[0].name, "1_1_generate_my_primary_key"); 64 | assert_eq!(stats.primary_tables[0].n_entries, Some(5)); 65 | assert_eq!(stats.secondary_tables.len(), 1); 66 | assert_eq!( 67 | stats.secondary_tables[0].name, 68 | "1_1_generate_my_secondary_key" 69 | ); 70 | assert_eq!(stats.secondary_tables[0].n_entries, Some(5)); 71 | 72 | // Count items 73 | let r = db.r_transaction().unwrap(); 74 | let len = r.len().primary::().unwrap(); 75 | assert_eq!(len, 5); 76 | 77 | // Drain items 78 | let rw = db.rw_transaction().unwrap(); 79 | let items = rw.drain().primary::().unwrap(); 80 | assert_eq!(items.len(), 5); 81 | rw.commit().unwrap(); 82 | 83 | // Count items 84 | let r = db.r_transaction().unwrap(); 85 | let len = r.len().primary::().unwrap(); 86 | assert_eq!(len, 0); 87 | 88 | let stats = db.redb_stats().unwrap(); 89 | assert_eq!(stats.primary_tables.len(), 1); 90 | assert_eq!(stats.primary_tables[0].name, "1_1_generate_my_primary_key"); 91 | assert_eq!(stats.primary_tables[0].n_entries, Some(0)); 92 | assert_eq!(stats.secondary_tables.len(), 1); 93 | assert_eq!( 94 | stats.secondary_tables[0].name, 95 | "1_1_generate_my_secondary_key" 96 | ); 97 | assert_eq!(stats.secondary_tables[0].n_entries, Some(0)); 98 | } 99 | -------------------------------------------------------------------------------- /tests/migrate/only_primary_key.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | 6 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] 7 | #[native_model(id = 1, version = 1)] 8 | #[native_db( 9 | primary_key(generate_my_primary_key -> String), 10 | )] 11 | struct ItemV1 { 12 | id: u32, 13 | name: String, 14 | } 15 | 16 | impl ItemV1 { 17 | #[allow(dead_code)] 18 | pub fn generate_my_primary_key(&self) -> String { 19 | format!("{}-{}", self.id, self.name) 20 | } 21 | } 22 | 23 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] 24 | #[native_model(id = 1, version = 2, from = ItemV1)] 25 | #[native_db( 26 | primary_key(generate_my_primary_key -> String), 27 | )] 28 | struct ItemV2 { 29 | id: u64, 30 | name: String, 31 | } 32 | 33 | impl From for ItemV2 { 34 | fn from(item: ItemV1) -> Self { 35 | ItemV2 { 36 | id: item.id as u64, 37 | name: item.name, 38 | } 39 | } 40 | } 41 | 42 | impl From for ItemV1 { 43 | fn from(item: ItemV2) -> Self { 44 | ItemV1 { 45 | id: item.id as u32, 46 | name: item.name, 47 | } 48 | } 49 | } 50 | 51 | impl ItemV2 { 52 | #[allow(dead_code)] 53 | pub fn generate_my_primary_key(&self) -> String { 54 | format!("{}-{}", self.id, self.name) 55 | } 56 | } 57 | 58 | #[test] 59 | fn test_migrate() { 60 | let tf = TmpFs::new().unwrap(); 61 | let mut models = Models::new(); 62 | models.define::().unwrap(); 63 | let db = Builder::new() 64 | .create(&models, tf.path("test").as_std_path()) 65 | .unwrap(); 66 | 67 | let item = ItemV1 { 68 | id: 1, 69 | name: "test".to_string(), 70 | }; 71 | 72 | let rw_txn = db.rw_transaction().unwrap(); 73 | rw_txn.insert(item).unwrap(); 74 | rw_txn.commit().unwrap(); 75 | 76 | let r_txn = db.r_transaction().unwrap(); 77 | 78 | let item: ItemV1 = r_txn.get().primary("1-test").unwrap().unwrap(); 79 | assert_eq!( 80 | item, 81 | ItemV1 { 82 | id: 1, 83 | name: "test".to_string(), 84 | } 85 | ); 86 | drop(r_txn); 87 | drop(db); 88 | 89 | let mut models = Models::new(); 90 | models.define::().unwrap(); 91 | models.define::().unwrap(); 92 | let db = Builder::new() 93 | .create(&models, tf.path("test").as_std_path()) 94 | .unwrap(); 95 | 96 | let rw = db.rw_transaction().unwrap(); 97 | rw.migrate::().unwrap(); 98 | rw.commit().unwrap(); 99 | 100 | let r_txn = db.r_transaction().unwrap(); 101 | let item: ItemV2 = r_txn.get().primary("1-test").unwrap().unwrap(); 102 | assert_eq!( 103 | item, 104 | ItemV2 { 105 | id: 1, 106 | name: "test".to_string(), 107 | } 108 | ); 109 | 110 | let stats = db.redb_stats().unwrap(); 111 | assert_eq!(stats.primary_tables.len(), 2); 112 | assert_eq!(stats.primary_tables[0].name, "1_1_generate_my_primary_key"); 113 | assert_eq!(stats.primary_tables[0].n_entries, Some(0)); 114 | assert_eq!(stats.primary_tables[1].name, "1_2_generate_my_primary_key"); 115 | assert_eq!(stats.primary_tables[1].n_entries, Some(1)); 116 | assert_eq!(stats.secondary_tables.len(), 0); 117 | } 118 | -------------------------------------------------------------------------------- /tests/query/auto_update_pk.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | 6 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 7 | #[native_model(id = 1, version = 1)] 8 | #[native_db] 9 | struct Item { 10 | #[primary_key] 11 | id: u32, 12 | name: String, 13 | } 14 | 15 | #[test] 16 | fn auto_update_non_existent() { 17 | let tf = TmpFs::new().unwrap(); 18 | let mut models = Models::new(); 19 | models.define::().unwrap(); 20 | let db = Builder::new() 21 | .create(&models, tf.path("test").as_std_path()) 22 | .unwrap(); 23 | 24 | let rw = db.rw_transaction().unwrap(); 25 | let result = rw.auto_update(Item { 26 | id: 1, 27 | name: "test".to_string(), 28 | }); 29 | assert!(result.unwrap().is_none()); 30 | rw.commit().unwrap(); 31 | } 32 | 33 | #[test] 34 | fn auto_update_existing() { 35 | let tf = TmpFs::new().unwrap(); 36 | let mut models = Models::new(); 37 | models.define::().unwrap(); 38 | let db = Builder::new() 39 | .create(&models, tf.path("test").as_std_path()) 40 | .unwrap(); 41 | 42 | let initial_item = Item { 43 | id: 1, 44 | name: "initial".to_string(), 45 | }; 46 | let updated_item = Item { 47 | id: 1, 48 | name: "updated".to_string(), 49 | }; 50 | 51 | // Insert initial item 52 | let rw = db.rw_transaction().unwrap(); 53 | rw.insert(initial_item.clone()).unwrap(); 54 | rw.commit().unwrap(); 55 | 56 | // Update the item 57 | let rw = db.rw_transaction().unwrap(); 58 | let old_value = rw.auto_update(updated_item.clone()).unwrap(); 59 | assert!(old_value.is_some()); 60 | assert_eq!(old_value.unwrap(), initial_item); 61 | 62 | // Verify the update 63 | let current: Item = rw.get().primary(1u32).unwrap().unwrap(); 64 | assert_eq!(current, updated_item); 65 | rw.commit().unwrap(); 66 | } 67 | 68 | #[test] 69 | fn auto_update_multiple() { 70 | let tf = TmpFs::new().unwrap(); 71 | let mut models = Models::new(); 72 | models.define::().unwrap(); 73 | let db = Builder::new() 74 | .create(&models, tf.path("test").as_std_path()) 75 | .unwrap(); 76 | 77 | // Insert multiple items 78 | let rw = db.rw_transaction().unwrap(); 79 | for i in 1..=3 { 80 | rw.insert(Item { 81 | id: i, 82 | name: format!("item{}", i), 83 | }) 84 | .unwrap(); 85 | } 86 | rw.commit().unwrap(); 87 | 88 | // Update middle item 89 | let rw = db.rw_transaction().unwrap(); 90 | let old_value = rw 91 | .auto_update(Item { 92 | id: 2, 93 | name: "updated".to_string(), 94 | }) 95 | .unwrap(); 96 | assert!(old_value.is_some()); 97 | assert_eq!( 98 | old_value.unwrap(), 99 | Item { 100 | id: 2, 101 | name: "item2".to_string() 102 | } 103 | ); 104 | 105 | // Verify other items unchanged 106 | let item1: Item = rw.get().primary(1u32).unwrap().unwrap(); 107 | assert_eq!(item1.name, "item1"); 108 | let item2: Item = rw.get().primary(2u32).unwrap().unwrap(); 109 | assert_eq!(item2.name, "updated"); 110 | let item3: Item = rw.get().primary(3u32).unwrap().unwrap(); 111 | assert_eq!(item3.name, "item3"); 112 | rw.commit().unwrap(); 113 | } 114 | -------------------------------------------------------------------------------- /tests/util.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use shortcut_assert_fs::TmpFs; 3 | 4 | #[test] 5 | fn test_builder() { 6 | let tf = TmpFs::new().unwrap(); 7 | // Create without error 8 | let mut _db = Builder::new() 9 | .create(&Models::new(), tf.path("test")) 10 | .unwrap(); 11 | } 12 | 13 | #[test] 14 | fn test_builder_with_set_cache_size() { 15 | let tf = TmpFs::new().unwrap(); 16 | // Create without error 17 | let mut builder = Builder::new(); 18 | let _db = builder 19 | .set_cache_size(100) 20 | .create(&Models::new(), tf.path("test")) 21 | .unwrap(); 22 | } 23 | 24 | #[test] 25 | fn test_open_unexisting_database() { 26 | let tf = TmpFs::new().unwrap(); 27 | // Open an unexisting database 28 | assert!(Builder::new() 29 | .open(&Models::new(), tf.path("test")) 30 | .is_err()); 31 | } 32 | 33 | #[test] 34 | fn test_open_existing_database() { 35 | let tf = TmpFs::new().unwrap(); 36 | 37 | // Create a database 38 | let builder = Builder::new(); 39 | let models = Models::new(); 40 | let db = builder.create(&models, tf.path("test")).unwrap(); 41 | drop(db); 42 | 43 | // Open an existing database 44 | let _db = Builder::new().open(&models, tf.path("test")).unwrap(); 45 | } 46 | 47 | use native_model::{native_model, Model}; 48 | use serde::{Deserialize, Serialize}; 49 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 50 | #[native_model(id = 1, version = 1)] 51 | #[native_db] 52 | struct Item1 { 53 | #[primary_key] 54 | id: u32, 55 | #[secondary_key(unique)] 56 | name: String, 57 | } 58 | 59 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 60 | #[native_model(id = 2, version = 1)] 61 | #[native_db] 62 | struct Item2 { 63 | #[primary_key] 64 | id: u32, 65 | #[secondary_key(optional)] 66 | id2: Option, 67 | #[secondary_key] 68 | name: String, 69 | } 70 | 71 | #[cfg(not(any(target_os = "android", target_os = "ios")))] 72 | #[test] 73 | fn create_local_database_for_tests() { 74 | let root_project_path = env!("CARGO_MANIFEST_DIR"); 75 | let tmp_data_dir_path = format!("{}/tests/data", root_project_path); 76 | 77 | std::fs::create_dir_all(tmp_data_dir_path.clone()).unwrap(); 78 | 79 | let database_path = format!("{}/db_x_x_x", tmp_data_dir_path); 80 | 81 | if std::fs::metadata(&database_path).is_ok() { 82 | std::fs::remove_file(&database_path).unwrap(); 83 | } 84 | 85 | let mut models = Models::new(); 86 | models.define::().unwrap(); 87 | models.define::().unwrap(); 88 | let db = Builder::new().create(&models, &database_path).unwrap(); 89 | let rw = db.rw_transaction().unwrap(); 90 | let item = Item1 { 91 | id: 1, 92 | name: "item1".to_string(), 93 | }; 94 | 95 | // Genereate 1000 Item2 with random values 96 | for i in 0..1000 { 97 | let id2 = if i % 2 == 0 { Some(i) } else { None }; 98 | let item = Item2 { 99 | id: i, 100 | id2, 101 | name: format!("item2_{}", i), 102 | }; 103 | rw.insert(item).unwrap(); 104 | } 105 | 106 | rw.insert(item).unwrap(); 107 | rw.commit().unwrap(); 108 | 109 | let ro = db.r_transaction().unwrap(); 110 | let len = ro.len().primary::().unwrap(); 111 | assert_eq!(len, 1); 112 | 113 | let len = ro.len().primary::().unwrap(); 114 | assert_eq!(len, 1000); 115 | } 116 | -------------------------------------------------------------------------------- /version_update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # How to use: `./version_update.sh 0.8.0` 4 | 5 | # How to test: 6 | # - Use docker `docker run -it --rm -v $(pwd):/mnt/native_db ubuntu bash` 7 | # - `/mnt/native_db` 8 | # - `./version_update.sh 0.8.0` 9 | 10 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 11 | 12 | # Bash script to update version for native_db and native_db_macro 13 | 14 | # Semantic release version obtained from argument 15 | NEW_VERSION=$1 16 | 17 | # Exit if NEW_VERSION is not set 18 | if [ -z "$NEW_VERSION" ]; then 19 | echo "NEW_VERSION argument not set" 20 | exit 1 21 | fi 22 | 23 | # Directories containing Cargo.toml files to update 24 | declare -a directories=("." "native_db_macro") 25 | 26 | for directory in "${directories[@]}" 27 | do 28 | # Check if Cargo.toml and README.md exist 29 | if [ -f "$directory/Cargo.toml" ] && [ -f "$directory/README.md" ]; then 30 | echo "Updating version in $directory/Cargo.toml to $NEW_VERSION" 31 | # Use sed to find and replace the version string in the Cargo.toml 32 | sed -i -E "s/^version = \"[0-9]+\.[0-9]+\.[0-9]+\"/version = \"$NEW_VERSION\"/g" "$directory/Cargo.toml" 33 | 34 | # Update the dependency version for native_db_macro in native_db's Cargo.toml 35 | if [ "$directory" == "." ]; then 36 | sed -i -E "s/native_db_macro = \{ version = \"[0-9]+\.[0-9]+\.[0-9]+\", path = \"native_db_macro\" \}/native_db_macro = { version = \"$NEW_VERSION\", path = \"native_db_macro\" }/g" "$directory/Cargo.toml" 37 | 38 | # Extract native_model version from Cargo.toml 39 | NATIVE_MODEL_VERSION=$(grep -oP '(?<=native_model = \{ version = ")[^"]*' "$directory/Cargo.toml") 40 | echo "Updating native_model version in $directory/Cargo.toml to $NATIVE_MODEL_VERSION" 41 | 42 | # Use sed to find and replace the version string in the README.md 43 | sed -i -E "s/native_db = \"[0-9]+\.[0-9]+\.[0-9]+\"/native_db = \"$NEW_VERSION\"/g" "$directory/README.md" 44 | sed -i -E "s/native_model = \"[0-9]+\.[0-9]+\.[0-9]+\"/native_model = \"$NATIVE_MODEL_VERSION\"/g" "$directory/README.md" 45 | 46 | # Replace on src/metadata/current_version.rs: const CURRENT_VERSION: &str = "x.x.x"; 47 | sed -i -E "s/pub const CURRENT_VERSION: \&str = \"[0-9]+\.[0-9]+\.[0-9]+\";/pub const CURRENT_VERSION: \&str = \"$NEW_VERSION\";/g" "$directory/src/metadata/current_version.rs" 48 | # Replace on src/metadata/current_native_model_version.rs: const CURRENT_NATIVE_MODEL_VERSION: &str = "x.x.x"; 49 | sed -i -E "s/pub const CURRENT_NATIVE_MODEL_VERSION: \&str = \"[0-9]+\.[0-9]+\.[0-9]+\";/pub const CURRENT_NATIVE_MODEL_VERSION: \&str = \"$NATIVE_MODEL_VERSION\";/g" "$directory/src/metadata/current_native_model_version.rs" 50 | 51 | # Replace on tests/metadata/current_version.rs: assert_eq!(metadata.current_native_model_version(), "x.x.x"); 52 | sed -i -E "s/assert_eq!\(metadata.current_native_model_version(), \"[0-9]+\.[0-9]+\.[0-9]+\";/assert_eq!\(metadata.current_native_model_version(), \"$NATIVE_MODEL_VERSION\";/g" "$directory/tests/metadata/current_version.rs" 53 | # Replace on tests/metadata/current_version.rs: assert_eq!(metadata.current_native_model_version(), Some("x.x.x")); 54 | sed -i -E "s/assert_eq!\(metadata.current_native_model_version(), Some\(\"[0-9]+\.[0-9]+\.[0-9]+\";/assert_eq!\(metadata.current_native_model_version(), Some\(\"$NATIVE_MODEL_VERSION\";/g" "$directory/tests/metadata/current_version.rs" 55 | fi 56 | fi 57 | done 58 | 59 | 60 | cd "$DIR/" 61 | 62 | # Commit 63 | git commit --all --message "chore: update version to $NEW_VERSION" 64 | git push -------------------------------------------------------------------------------- /.github/workflows/build_test_android.yml: -------------------------------------------------------------------------------- 1 | name: Android 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main, next ] 8 | schedule: 9 | # At 23:00 on Thursday. 10 | - cron: '0 23 * * 4' 11 | 12 | env: 13 | RUST_BACKTRACE: full 14 | 15 | jobs: 16 | build_test: 17 | runs-on: macos-13 18 | strategy: 19 | fail-fast: true 20 | matrix: 21 | #target: [i686-linux-android, aarch64-linux-android, armv7-linux-androideabi, thumbv7neon-linux-androideabi, x86_64-linux-android] 22 | #target: [aarch64-linux-android, x86_64-linux-android, armv7-linux-androideabi] 23 | toolchain: [stable] 24 | profile: 25 | - target: aarch64-linux-android 26 | image: "system-images;android-34;google_apis;x86_64" 27 | steps: 28 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 29 | - name: Setup Rust 30 | uses: dtolnay/rust-toolchain@stable 31 | with: 32 | toolchain: ${{ matrix.toolchain }} 33 | targets: ${{ matrix.profile.target }} 34 | - name: Cache emulator 35 | uses: actions/cache@v4 36 | if: always() 37 | with: 38 | path: | 39 | /Users/runner/Library/Android/sdk/emulator 40 | key: android-emulator-global 41 | - name: Cache system-images 42 | uses: actions/cache@v4 43 | if: always() 44 | with: 45 | path: | 46 | /Users/runner/Library/Android/sdk/system-images 47 | key: android-system-images-global-${{ matrix.profile.image }} 48 | - run: echo "/Users/runner/.cargo/bin" >> $GITHUB_PATH 49 | - run: echo "/Users/runner/Library/Android/sdk/emulator" >> $GITHUB_PATH 50 | - run: echo "/Users/runner/Library/Android/sdk/platform-tools" >> $GITHUB_PATH 51 | - run: echo "/Users/runner/Library/Android/sdk/cmdline-tools/latest/bin" >> $GITHUB_PATH 52 | # Install utilities 53 | - name: Cache cargo install 54 | uses: actions/cache@v4 55 | if: always() 56 | with: 57 | path: | 58 | ~/.cargo/bin/ 59 | key: cargo-global-${{ matrix.toolchain }}-${{ github.ref }}-${{ hashFiles('**/Cargo.lock') }} 60 | - run: if ! command -v cargo-dinghy &> /dev/null; then cargo install --version 0.8.1 cargo-dinghy; fi 61 | - run: if ! command -v just &> /dev/null; then cargo install --version 1.25.2 just; fi 62 | - run: just --version 63 | - uses: hustcer/setup-nu@v3.20 64 | with: 65 | version: '0.105.1' 66 | env: 67 | GITHUB_TOKEN: ${{ secrets.PAT_GLOBAL }} 68 | # End install utilities 69 | - run: adb start-server 70 | - run: sdkmanager emulator 71 | - run: echo "/Users/runner/Library/Android/sdk/emulator" >> $GITHUB_PATH 72 | - run: yes | sdkmanager --install "${{ matrix.profile.image }}" 73 | # - run: sdkmanager --list_installed 74 | # - run: sdkmanager --list 75 | - run: echo "no" | avdmanager create avd -n testDevice -k "${{ matrix.profile.image }}" 76 | - run: emulator -avd testDevice -no-audio -no-window -gpu swiftshader_indirect -no-snapshot -no-boot-anim -camera-back none -camera-front none -selinux permissive -qemu -m 2048 & 77 | - run: adb wait-for-device 78 | # - name: just test_mobile_all_platforms 79 | # - run: emulator -list-avds 80 | # - run: avdmanager list 81 | - run: just test_mobile_all_devices 82 | # Skip doctests on Android due to cargo-dinghy permission issues and "text file busy" errors 83 | - run: just test_android_lib 84 | -------------------------------------------------------------------------------- /tests/upgrade_error.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | 6 | #[derive(Serialize, Deserialize, Debug)] 7 | #[native_model(id = 1, version = 1)] 8 | #[native_db] 9 | struct TestModel { 10 | #[primary_key] 11 | id: u32, 12 | name: String, 13 | } 14 | 15 | #[test] 16 | fn test_version_mismatch_error() { 17 | // Create a temporary directory for the test database 18 | let tf = TmpFs::new().unwrap(); 19 | let db_path = tf.path("test.db"); 20 | 21 | // First, create a database with current versions 22 | { 23 | let mut models = Models::new(); 24 | models.define::().unwrap(); 25 | let db = Builder::new().create(&models, &db_path).unwrap(); 26 | drop(db); 27 | } 28 | 29 | // Now manually modify the metadata to simulate an old version 30 | { 31 | let builder = redb::Builder::new(); 32 | let db = builder.open(&db_path).unwrap(); 33 | let write_tx = db.begin_write().unwrap(); 34 | { 35 | let mut table = write_tx 36 | .open_table(redb::TableDefinition::<&str, &str>::new("metadata")) 37 | .unwrap(); 38 | table.insert("version_native_db", "0.7.0").unwrap(); 39 | table.insert("version_native_model", "0.4.18").unwrap(); 40 | } 41 | write_tx.commit().unwrap(); 42 | drop(db); 43 | } 44 | 45 | // Try to open the database again and expect an upgrade error 46 | let mut models = Models::new(); 47 | models.define::().unwrap(); 48 | 49 | match Builder::new().open(&models, &db_path) { 50 | Err(db_type::Error::UpgradeRequired(upgrade_err)) => { 51 | let error_string = upgrade_err.to_string(); 52 | assert!(error_string.contains("Database upgrade required:")); 53 | assert!(error_string.contains("Native DB: 0.7.0 → 0.8.1")); 54 | assert!(error_string.contains("Native Model: 0.4.18 → 0.4.19")); 55 | 56 | // Check the detailed error fields 57 | assert_eq!( 58 | upgrade_err.native_db_version, 59 | Some(("0.7.0".to_string(), "0.8.1".to_string())) 60 | ); 61 | assert_eq!( 62 | upgrade_err.native_model_version, 63 | Some(("0.4.18".to_string(), "0.4.19".to_string())) 64 | ); 65 | assert_eq!(upgrade_err.redb_version, None); 66 | } 67 | Ok(_) => panic!("Expected upgrade error but database opened successfully"), 68 | Err(e) => panic!("Expected UpgradeRequired error but got: {:?}", e), 69 | } 70 | } 71 | 72 | #[test] 73 | fn test_no_upgrade_needed() { 74 | // Create a temporary directory for the test database 75 | let tf = TmpFs::new().unwrap(); 76 | let db_path = tf.path("test.db"); 77 | 78 | // Create and open a database with current versions 79 | let mut models = Models::new(); 80 | models.define::().unwrap(); 81 | 82 | // Create the database 83 | { 84 | let _db = Builder::new().create(&models, &db_path).unwrap(); 85 | } 86 | 87 | // Open the database again - should succeed without upgrade error 88 | let _db = Builder::new().open(&models, &db_path).unwrap(); 89 | } 90 | 91 | // TODO: Add test for redb upgrade error scenario 92 | // Testing redb upgrade error would require creating a database with 93 | // redb format version 1, which is not easily doable in a unit test. 94 | // This would typically be tested with integration tests using actual old database files. 95 | -------------------------------------------------------------------------------- /tests/custom_type/custom.rs: -------------------------------------------------------------------------------- 1 | use native_db::{ 2 | db_type::{Key, ToKey}, 3 | native_db, Builder, Models, 4 | }; 5 | use native_model::{native_model, Model}; 6 | use serde::{Deserialize, Serialize}; 7 | use shortcut_assert_fs::TmpFs; 8 | 9 | #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] 10 | struct City(String); 11 | 12 | impl ToKey for City { 13 | fn to_key(&self) -> Key { 14 | Key::new(self.0.as_bytes().to_vec()) 15 | } 16 | 17 | fn key_names() -> Vec { 18 | vec!["City".to_string()] 19 | } 20 | } 21 | 22 | // Test genrate fields: 23 | // - primary_key 24 | // - secondary_keys (unique) 25 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 26 | #[native_model(id = 1, version = 1)] 27 | #[native_db] 28 | struct ItemFields { 29 | #[primary_key] 30 | city1: City, 31 | #[secondary_key(unique)] 32 | city2: City, 33 | #[secondary_key(optional)] 34 | city3: Option, 35 | } 36 | 37 | #[test] 38 | fn insert_item_fields() { 39 | let item = ItemFields { 40 | city1: City("New York".to_string()), 41 | city2: City("New York".to_string()), 42 | city3: Some(City("New York".to_string())), 43 | }; 44 | 45 | let tf = TmpFs::new().unwrap(); 46 | let mut models = Models::new(); 47 | models.define::().unwrap(); 48 | let db = Builder::new() 49 | .create(&models, tf.path("test").as_std_path()) 50 | .unwrap(); 51 | 52 | let rw = db.rw_transaction().unwrap(); 53 | rw.insert(item.clone()).unwrap(); 54 | rw.commit().unwrap(); 55 | 56 | let r = db.r_transaction().unwrap(); 57 | let result_item = r 58 | .get() 59 | .secondary(ItemFieldsKey::city2, item.city2.clone()) 60 | .unwrap() 61 | .unwrap(); 62 | assert_eq!(item, result_item); 63 | } 64 | 65 | // Test genrate functions: 66 | // - primary_key 67 | // - secondary_keys (unique) 68 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 69 | #[native_model(id = 1, version = 1)] 70 | #[native_db( 71 | primary_key(m_city1 -> City), 72 | secondary_key(m_city2 -> City, unique), 73 | secondary_key(m_city2_ref -> City, unique), 74 | secondary_key(m_city3 -> Option, optional), 75 | )] 76 | struct ItemFunctions { 77 | city1: City, 78 | city2: City, 79 | city3: Option, 80 | } 81 | 82 | impl ItemFunctions { 83 | fn m_city1(&self) -> City { 84 | self.city1.clone() 85 | } 86 | 87 | fn m_city2(&self) -> City { 88 | self.city2.clone() 89 | } 90 | 91 | fn m_city2_ref(&self) -> &City { 92 | &self.city2 93 | } 94 | 95 | fn m_city3(&self) -> Option { 96 | self.city3.clone() 97 | } 98 | } 99 | 100 | #[test] 101 | fn test_item_functions() { 102 | let item = ItemFunctions { 103 | city1: City("New York".to_string()), 104 | city2: City("New York".to_string()), 105 | city3: Some(City("New York".to_string())), 106 | }; 107 | 108 | let tf = TmpFs::new().unwrap(); 109 | let mut models = Models::new(); 110 | models.define::().unwrap(); 111 | let db = Builder::new() 112 | .create(&models, tf.path("test").as_std_path()) 113 | .unwrap(); 114 | 115 | let rw = db.rw_transaction().unwrap(); 116 | rw.insert(item.clone()).unwrap(); 117 | rw.commit().unwrap(); 118 | 119 | let r = db.r_transaction().unwrap(); 120 | let result_item = r 121 | .get() 122 | .secondary(ItemFunctionsKey::m_city2, item.city2.clone()) 123 | .unwrap() 124 | .unwrap(); 125 | assert_eq!(item, result_item); 126 | } 127 | -------------------------------------------------------------------------------- /src/db_type/error.rs: -------------------------------------------------------------------------------- 1 | use crate::{db_type, watch}; 2 | use thiserror::Error; 3 | 4 | #[derive(Debug, Error)] 5 | pub enum Error { 6 | #[error("Redb error")] 7 | Redb(#[from] Box), 8 | 9 | #[error("Redb database error")] 10 | RedbDatabaseError(#[from] Box), 11 | 12 | #[error("Redb transaction error")] 13 | RedbTransactionError(#[from] Box), 14 | 15 | #[error("Redb storage error")] 16 | RedbStorageError(#[from] redb::StorageError), 17 | 18 | #[error("Redb table error")] 19 | RedbTableError(#[from] redb::TableError), 20 | 21 | #[error("Redb commit error")] 22 | RedbCommitError(#[from] redb::CommitError), 23 | 24 | #[error("Redb compaction error")] 25 | RedbCompactionError(#[from] redb::CompactionError), 26 | 27 | #[error(transparent)] 28 | UpgradeRequired(#[from] Box), 29 | 30 | #[error("IO error")] 31 | Io(#[from] std::io::Error), 32 | 33 | #[error("Table definition not found {table}")] 34 | TableDefinitionNotFound { table: String }, 35 | 36 | #[error("Secondary key definition not found {table} {key}")] 37 | SecondaryKeyDefinitionNotFound { table: String, key: String }, 38 | 39 | #[error("Secondary key constraint mismatch {table} {key} got: {got:?}")] 40 | SecondaryKeyConstraintMismatch { 41 | table: String, 42 | key: String, 43 | got: db_type::KeyOptions, 44 | }, 45 | 46 | #[error("The secondary key {key_name} is not unique ")] 47 | NotUniqueSecondaryKey { key_name: String }, 48 | 49 | // TODO: key with key name. 50 | #[error("Key not found {key:?}")] 51 | KeyNotFound { key: Vec }, 52 | 53 | #[error("Primary key associated with the secondary key not found")] 54 | PrimaryKeyNotFound, 55 | 56 | #[error("Duplicate key for \"{key_name}\"")] 57 | DuplicateKey { key_name: String }, 58 | 59 | #[error("Mismatched key type for \"{key_name}\" expected {expected_types:?} got {got_types:?} during {operation:?}")] 60 | MismatchedKeyType { 61 | key_name: String, 62 | expected_types: Vec, 63 | got_types: Vec, 64 | operation: String, 65 | }, 66 | 67 | #[error("Watch event error")] 68 | WatchEventError(#[from] watch::WatchEventError), 69 | 70 | #[error("Max watcher reached (should be impossible)")] 71 | MaxWatcherReached, 72 | 73 | #[error("You can not migrate the table {0} because it is a legacy model")] 74 | MigrateLegacyModel(String), 75 | 76 | #[error("Model error")] 77 | ModelError(#[from] Box), 78 | 79 | #[error("Fail to remove secondary key: {0}")] 80 | RemoveSecondaryKeyError(String), 81 | 82 | #[error("Inccorect input data it does not match the model")] 83 | IncorrectInputData { value: Vec }, 84 | 85 | #[error("Upgrade migration error: {context}")] 86 | UpgradeMigration { 87 | context: String, 88 | #[source] 89 | source: Box, 90 | }, 91 | } 92 | 93 | impl From for Error { 94 | fn from(e: redb::Error) -> Self { 95 | Error::Redb(Box::new(e)) 96 | } 97 | } 98 | 99 | impl From for Error { 100 | fn from(e: redb::DatabaseError) -> Self { 101 | Error::RedbDatabaseError(Box::new(e)) 102 | } 103 | } 104 | 105 | impl From for Error { 106 | fn from(e: redb::TransactionError) -> Self { 107 | Error::RedbTransactionError(Box::new(e)) 108 | } 109 | } 110 | 111 | impl From for Error { 112 | fn from(e: native_model::Error) -> Self { 113 | Error::ModelError(Box::new(e)) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tests/migrate/with_other_model.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | 6 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] 7 | #[native_model(id = 1, version = 1)] 8 | #[native_db] 9 | struct ItemV1 { 10 | #[primary_key] 11 | id: u32, 12 | name: String, 13 | } 14 | 15 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] 16 | #[native_model(id = 1, version = 2, from = ItemV1)] 17 | #[native_db] 18 | struct ItemV2 { 19 | #[primary_key] 20 | id: u32, 21 | name_v2: String, 22 | } 23 | 24 | impl From for ItemV2 { 25 | fn from(item: ItemV1) -> Self { 26 | ItemV2 { 27 | id: item.id, 28 | name_v2: item.name, 29 | } 30 | } 31 | } 32 | 33 | impl From for ItemV1 { 34 | fn from(item: ItemV2) -> Self { 35 | ItemV1 { 36 | id: item.id, 37 | name: item.name_v2, 38 | } 39 | } 40 | } 41 | 42 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] 43 | #[native_model(id = 2, version = 1)] 44 | #[native_db] 45 | struct Item2 { 46 | #[primary_key] 47 | id: u32, 48 | name2: String, 49 | } 50 | 51 | #[test] 52 | fn test_migrate() { 53 | let tf = TmpFs::new().unwrap(); 54 | let mut models = Models::new(); 55 | models.define::().unwrap(); 56 | models.define::().unwrap(); 57 | let db = Builder::new() 58 | .create(&models, tf.path("test").as_std_path()) 59 | .unwrap(); 60 | 61 | let item_2 = Item2 { 62 | id: 1, 63 | name2: "test2".to_string(), 64 | }; 65 | let rw_txn = db.rw_transaction().unwrap(); 66 | rw_txn.insert(item_2).unwrap(); 67 | rw_txn.commit().unwrap(); 68 | 69 | let item = ItemV1 { 70 | id: 1, 71 | name: "test".to_string(), 72 | }; 73 | 74 | let rw_txn = db.rw_transaction().unwrap(); 75 | rw_txn.insert(item).unwrap(); 76 | rw_txn.commit().unwrap(); 77 | 78 | let r_txn = db.r_transaction().unwrap(); 79 | 80 | let item: ItemV1 = r_txn.get().primary(1u32).unwrap().unwrap(); 81 | assert_eq!( 82 | item, 83 | ItemV1 { 84 | id: 1, 85 | name: "test".to_string(), 86 | } 87 | ); 88 | drop(r_txn); 89 | drop(db); 90 | 91 | let mut models = Models::new(); 92 | models.define::().unwrap(); 93 | models.define::().unwrap(); 94 | models.define::().unwrap(); 95 | let db = Builder::new() 96 | .create(&models, tf.path("test").as_std_path()) 97 | .unwrap(); 98 | 99 | let rw = db.rw_transaction().unwrap(); 100 | rw.migrate::().unwrap(); 101 | rw.commit().unwrap(); 102 | 103 | let r_txn = db.r_transaction().unwrap(); 104 | let item: ItemV2 = r_txn.get().primary(1u32).unwrap().unwrap(); 105 | assert_eq!( 106 | item, 107 | ItemV2 { 108 | id: 1, 109 | name_v2: "test".to_string(), 110 | } 111 | ); 112 | 113 | let item: Item2 = r_txn.get().primary(1u32).unwrap().unwrap(); 114 | assert_eq!( 115 | item, 116 | Item2 { 117 | id: 1, 118 | name2: "test2".to_string(), 119 | } 120 | ); 121 | 122 | let stats = db.redb_stats().unwrap(); 123 | assert_eq!(stats.primary_tables.len(), 3); 124 | assert_eq!(stats.primary_tables[0].name, "1_1_id"); 125 | assert_eq!(stats.primary_tables[0].n_entries, Some(0)); 126 | assert_eq!(stats.primary_tables[1].name, "1_2_id"); 127 | assert_eq!(stats.primary_tables[1].n_entries, Some(1)); 128 | assert_eq!(stats.primary_tables[2].name, "2_1_id"); 129 | assert_eq!(stats.primary_tables[2].n_entries, Some(1)); 130 | assert_eq!(stats.secondary_tables.len(), 0); 131 | } 132 | -------------------------------------------------------------------------------- /examples/upgrade.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::fs; 5 | 6 | // Version 1 of our model 7 | #[derive(Serialize, Deserialize, Debug)] 8 | #[native_model(id = 1, version = 1)] 9 | #[native_db] 10 | struct PersonV1 { 11 | #[primary_key] 12 | id: u32, 13 | name: String, 14 | } 15 | 16 | // Version 2 of our model - adds an age field 17 | #[derive(Serialize, Deserialize, Debug)] 18 | #[native_model(id = 1, version = 2)] 19 | #[native_db] 20 | struct PersonV2 { 21 | #[primary_key] 22 | id: u32, 23 | name: String, 24 | age: u32, 25 | } 26 | 27 | // Implement conversion from V1 to V2 28 | impl From for PersonV2 { 29 | fn from(v1: PersonV1) -> Self { 30 | PersonV2 { 31 | id: v1.id, 32 | name: v1.name, 33 | age: 0, // Default age for migrated records 34 | } 35 | } 36 | } 37 | 38 | fn main() -> Result<(), db_type::Error> { 39 | let db_path = "example_upgrade.db"; 40 | 41 | // Clean up any existing files 42 | let _ = fs::remove_file(db_path); 43 | let _ = fs::remove_file(format!("{}.upgrading", db_path)); 44 | let _ = fs::remove_file(format!("{}.old", db_path)); 45 | 46 | // Step 1: Create a database with V1 model 47 | println!("Creating database with PersonV1 model..."); 48 | { 49 | let mut models = Models::new(); 50 | models.define::()?; 51 | let db = Builder::new().create(&models, db_path)?; 52 | 53 | let txn = db.rw_transaction()?; 54 | txn.insert(PersonV1 { 55 | id: 1, 56 | name: "Alice".to_string(), 57 | })?; 58 | txn.insert(PersonV1 { 59 | id: 2, 60 | name: "Bob".to_string(), 61 | })?; 62 | txn.commit()?; 63 | 64 | println!("Inserted 2 PersonV1 records"); 65 | } 66 | 67 | // Step 2: Upgrade the database to V2 model 68 | println!("\nUpgrading database to PersonV2 model..."); 69 | let mut new_models = Models::new(); 70 | new_models.define::()?; 71 | 72 | let upgraded_db = Builder::new().upgrade(&new_models, db_path, |new_txn| { 73 | println!("Migration closure started..."); 74 | 75 | // Open the old database 76 | let mut old_models = Models::new(); 77 | old_models.define::()?; 78 | let old_db = Builder::new().open(&old_models, db_path)?; 79 | 80 | // Read all V1 records 81 | let old_txn = old_db.r_transaction()?; 82 | let mut count = 0; 83 | 84 | for result in old_txn.scan().primary()?.all()? { 85 | let person_v1: PersonV1 = result?; 86 | println!(" Migrating: {:?}", person_v1); 87 | 88 | // Convert V1 to V2 89 | let person_v2: PersonV2 = person_v1.into(); 90 | 91 | // Insert into new database 92 | new_txn.insert(person_v2)?; 93 | count += 1; 94 | } 95 | 96 | println!("Migration completed: {} records migrated", count); 97 | Ok(()) 98 | })?; 99 | 100 | // Step 3: Verify the upgraded database 101 | println!("\nVerifying upgraded database..."); 102 | let read_txn = upgraded_db.r_transaction()?; 103 | 104 | for result in read_txn.scan().primary()?.all()? { 105 | let person: PersonV2 = result?; 106 | println!( 107 | " PersonV2: id={}, name={}, age={}", 108 | person.id, person.name, person.age 109 | ); 110 | } 111 | 112 | // Check that backup was created 113 | if fs::metadata(format!("{}.old", db_path)).is_ok() { 114 | println!("\nBackup database created: {}.old", db_path); 115 | } 116 | 117 | // Clean up 118 | let _ = fs::remove_file(db_path); 119 | let _ = fs::remove_file(format!("{}.old", db_path)); 120 | 121 | Ok(()) 122 | } 123 | -------------------------------------------------------------------------------- /examples/major_upgrade/tests/test_main.rs: -------------------------------------------------------------------------------- 1 | use major_upgrade::main_old::main_old; 2 | use major_upgrade::new_main::main; 3 | 4 | #[test] 5 | fn test_main_creates_new_database() { 6 | // Test creating a new database with current version 7 | let db_path = std::env::temp_dir().join("test_new_db.db"); 8 | 9 | // Clean up if exists 10 | if db_path.exists() { 11 | std::fs::remove_file(&db_path).ok(); 12 | } 13 | 14 | // Create new database 15 | let result = main(&db_path); 16 | assert!(result.is_ok(), "Should create new database successfully"); 17 | 18 | // Verify database exists 19 | assert!(db_path.exists(), "Database file should exist"); 20 | 21 | // Run main again - should open existing database 22 | let result = main(&db_path); 23 | assert!(result.is_ok(), "Should open existing database successfully"); 24 | 25 | // Clean up 26 | std::fs::remove_file(&db_path).ok(); 27 | } 28 | 29 | #[test] 30 | fn test_main_upgrades_old_database() { 31 | // Test upgrading from v0.8.x to current version 32 | let db_path = std::env::temp_dir().join("test_upgrade_db.db"); 33 | 34 | // Clean up if exists 35 | if db_path.exists() { 36 | std::fs::remove_file(&db_path).ok(); 37 | } 38 | 39 | // First create an old database 40 | let result = main_old(&db_path); 41 | assert!(result.is_ok(), "Should create old database successfully"); 42 | assert!(db_path.exists(), "Old database should exist"); 43 | 44 | // Now run main which should trigger upgrade 45 | let result = main(&db_path); 46 | assert!(result.is_ok(), "Should upgrade database successfully"); 47 | 48 | // Verify we can open with current version 49 | let result = main(&db_path); 50 | assert!(result.is_ok(), "Should open upgraded database successfully"); 51 | 52 | // Clean up 53 | std::fs::remove_file(&db_path).ok(); 54 | } 55 | 56 | #[test] 57 | fn test_upgrade_preserves_data() { 58 | use major_upgrade::models::current_version::CurrentModel; 59 | use major_upgrade::models::v08x::V08xModel; 60 | 61 | let db_path = std::env::temp_dir().join("test_data_preservation.db"); 62 | 63 | // Clean up if exists 64 | if db_path.exists() { 65 | std::fs::remove_file(&db_path).ok(); 66 | } 67 | 68 | // Create old database with specific data 69 | { 70 | let mut models = native_db_v0_8_x::Models::new(); 71 | models.define::().unwrap(); 72 | 73 | let db = native_db_v0_8_x::Builder::new() 74 | .create(&models, &db_path) 75 | .unwrap(); 76 | 77 | let rw = db.rw_transaction().unwrap(); 78 | rw.insert(V08xModel { 79 | id: 42, 80 | name: "Preserved Item".to_string(), 81 | }) 82 | .unwrap(); 83 | rw.insert(V08xModel { 84 | id: 99, 85 | name: "Another Preserved".to_string(), 86 | }) 87 | .unwrap(); 88 | rw.commit().unwrap(); 89 | } 90 | 91 | // Run upgrade 92 | let result = main(&db_path); 93 | assert!(result.is_ok(), "Should upgrade successfully"); 94 | 95 | // Verify data was preserved 96 | { 97 | let mut models = native_db_current::Models::new(); 98 | models.define::().unwrap(); 99 | 100 | let db = native_db_current::Builder::new() 101 | .open(&models, &db_path) 102 | .unwrap(); 103 | 104 | let r = db.r_transaction().unwrap(); 105 | 106 | let item1: CurrentModel = r.get().primary(42u32).unwrap().unwrap(); 107 | assert_eq!(item1.id, 42); 108 | assert_eq!(item1.name, "Preserved Item"); 109 | 110 | let item2: CurrentModel = r.get().primary(99u32).unwrap().unwrap(); 111 | assert_eq!(item2.id, 99); 112 | assert_eq!(item2.name, "Another Preserved"); 113 | 114 | let count = r.len().primary::().unwrap(); 115 | assert_eq!(count, 2, "Should have exactly 2 items after migration"); 116 | } 117 | 118 | // Clean up 119 | std::fs::remove_file(&db_path).ok(); 120 | } 121 | -------------------------------------------------------------------------------- /tests/watch/watch_optional.rs: -------------------------------------------------------------------------------- 1 | use native_db::watch::Event; 2 | use native_db::*; 3 | use native_model::{native_model, Model}; 4 | use serde::{Deserialize, Serialize}; 5 | use shortcut_assert_fs::TmpFs; 6 | 7 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] 8 | #[native_model(id = 1, version = 1)] 9 | #[native_db] 10 | struct ItemAOptional { 11 | #[primary_key] 12 | id: u32, 13 | #[secondary_key(unique, optional)] 14 | name: Option, 15 | } 16 | 17 | #[test] 18 | fn watch_one_secondary_key_some() { 19 | let tf = TmpFs::new().unwrap(); 20 | 21 | let mut models: Models = Models::new(); 22 | models.define::().unwrap(); 23 | let db = Builder::new() 24 | .create(&models, tf.path("test").as_std_path()) 25 | .unwrap(); 26 | 27 | let a = ItemAOptional { 28 | id: 1, 29 | name: Some("a".to_string()), 30 | }; 31 | 32 | let (recv, _) = db 33 | .watch() 34 | .get() 35 | .secondary::(ItemAOptionalKey::name, Some("a")) 36 | .unwrap(); 37 | 38 | let rw = db.rw_transaction().unwrap(); 39 | rw.insert(a.clone()).unwrap(); 40 | rw.commit().unwrap(); 41 | 42 | for _ in 0..1 { 43 | let inner_event: ItemAOptional = 44 | if let Event::Insert(event) = recv.recv_timeout(super::TIMEOUT).unwrap() { 45 | event.inner().unwrap() 46 | } else { 47 | panic!("wrong event") 48 | }; 49 | assert_eq!(inner_event, a); 50 | } 51 | assert!(recv.try_recv().is_err()); 52 | } 53 | 54 | #[test] 55 | fn watch_one_secondary_key_none() { 56 | let tf = TmpFs::new().unwrap(); 57 | 58 | let mut models = Models::new(); 59 | models.define::().unwrap(); 60 | let db = Builder::new() 61 | .create(&models, tf.path("test").as_std_path()) 62 | .unwrap(); 63 | 64 | let a = ItemAOptional { id: 1, name: None }; 65 | 66 | let (recv, _) = db 67 | .watch() 68 | .get() 69 | .secondary::(ItemAOptionalKey::name, Some("a")) 70 | .unwrap(); 71 | 72 | let rw = db.rw_transaction().unwrap(); 73 | rw.insert(a.clone()).unwrap(); 74 | rw.commit().unwrap(); 75 | 76 | for _ in 0..1 { 77 | let result = recv.recv_timeout(super::TIMEOUT); 78 | assert!(result.is_err()); 79 | assert!(matches!( 80 | result.unwrap_err(), 81 | std::sync::mpsc::RecvTimeoutError::Timeout 82 | )); 83 | } 84 | assert!(recv.try_recv().is_err()); 85 | } 86 | 87 | #[test] 88 | fn watch_start_with_by_key() { 89 | let tf = TmpFs::new().unwrap(); 90 | 91 | let mut models = Models::new(); 92 | models.define::().unwrap(); 93 | let db = Builder::new() 94 | .create(&models, tf.path("test").as_std_path()) 95 | .unwrap(); 96 | 97 | let item_a_1_k = ItemAOptional { 98 | id: 1, 99 | name: Some("a_1".to_string()), 100 | }; 101 | let item_a_2_k = ItemAOptional { 102 | id: 2, 103 | name: Some("a_2".to_string()), 104 | }; 105 | let item_a_3_k = ItemAOptional { 106 | id: 3, 107 | name: Some("b_1".to_string()), 108 | }; 109 | 110 | let (recv, _) = db 111 | .watch() 112 | .scan() 113 | .secondary(ItemAOptionalKey::name) 114 | .start_with::(Some("a")) 115 | .unwrap(); 116 | 117 | let rw = db.rw_transaction().unwrap(); 118 | rw.insert(item_a_1_k.clone()).unwrap(); 119 | rw.insert(item_a_2_k.clone()).unwrap(); 120 | rw.insert(item_a_3_k.clone()).unwrap(); 121 | rw.commit().unwrap(); 122 | 123 | for _ in 0..2 { 124 | let inner_event: ItemAOptional = 125 | if let Event::Insert(event) = recv.recv_timeout(super::TIMEOUT).unwrap() { 126 | event.inner().unwrap() 127 | } else { 128 | panic!("wrong event") 129 | }; 130 | assert!(inner_event == item_a_1_k || inner_event == item_a_2_k); 131 | } 132 | assert!(recv.try_recv().is_err()); 133 | } 134 | -------------------------------------------------------------------------------- /src/transaction/query/len.rs: -------------------------------------------------------------------------------- 1 | use crate::db_type::{KeyOptions, Result, ToInput, ToKeyDefinition}; 2 | use crate::transaction::internal::private_readable_transaction::PrivateReadableTransaction; 3 | use crate::transaction::internal::r_transaction::InternalRTransaction; 4 | use crate::transaction::internal::rw_transaction::InternalRwTransaction; 5 | 6 | /// Get the number of values in the database. 7 | pub struct RLen<'db, 'txn> { 8 | pub(crate) internal: &'txn InternalRTransaction<'db>, 9 | } 10 | 11 | impl RLen<'_, '_> { 12 | /// Get the number of values. 13 | /// 14 | /// # Example 15 | /// ```rust 16 | /// use native_db::*; 17 | /// use native_db::native_model::{native_model, Model}; 18 | /// use serde::{Deserialize, Serialize}; 19 | /// 20 | /// #[derive(Serialize, Deserialize)] 21 | /// #[native_model(id=1, version=1)] 22 | /// #[native_db] 23 | /// struct Data { 24 | /// #[primary_key] 25 | /// id: u64, 26 | /// } 27 | /// 28 | /// fn main() -> Result<(), db_type::Error> { 29 | /// let mut models = Models::new(); 30 | /// models.define::()?; 31 | /// let db = Builder::new().create_in_memory(&models)?; 32 | /// 33 | /// // Open a read transaction 34 | /// let r = db.r_transaction()?; 35 | /// 36 | /// // Get all values 37 | /// let _number:u64 = r.len().primary::()?; 38 | /// Ok(()) 39 | /// } 40 | /// ``` 41 | pub fn primary(&self) -> Result { 42 | let model = T::native_db_model(); 43 | let result = self.internal.primary_len(model)?; 44 | Ok(result) 45 | } 46 | 47 | /// Get the number of values by secondary key. 48 | /// 49 | /// Anatomy of a secondary key it is a `enum` with the following structure: `Key::`. 50 | /// 51 | /// If the secondary key is [`optional`](struct.Builder.html#optional) you will 52 | /// get all values that have the secondary key set. 53 | /// 54 | /// # Example 55 | /// ```rust 56 | /// use native_db::*; 57 | /// use native_db::native_model::{native_model, Model}; 58 | /// use serde::{Deserialize, Serialize}; 59 | /// 60 | /// #[derive(Serialize, Deserialize)] 61 | /// #[native_model(id=1, version=1)] 62 | /// #[native_db] 63 | /// struct Data { 64 | /// #[primary_key] 65 | /// id: u64, 66 | /// #[secondary_key(optional)] 67 | /// name: Option, 68 | /// } 69 | /// 70 | /// 71 | /// fn main() -> Result<(), db_type::Error> { 72 | /// let mut models = Models::new(); 73 | /// models.define::()?; 74 | /// let db = Builder::new().create_in_memory(&models)?; 75 | /// 76 | /// // Open a read transaction 77 | /// let r = db.r_transaction()?; 78 | /// 79 | /// // Get the number of values with the secondary key set 80 | /// let _number:u64 = r.len().secondary::(DataKey::name)?; 81 | /// Ok(()) 82 | /// } 83 | /// ``` 84 | pub fn secondary(&self, key_def: impl ToKeyDefinition) -> Result { 85 | let model = T::native_db_model(); 86 | let result = self.internal.secondary_len(model, key_def)?; 87 | Ok(result) 88 | } 89 | } 90 | 91 | pub struct RwLen<'db, 'txn> { 92 | pub(crate) internal: &'txn InternalRwTransaction<'db>, 93 | } 94 | 95 | impl RwLen<'_, '_> { 96 | /// Get the number of values. 97 | /// 98 | /// Same as [`RLen::primary()`](struct.RLen.html#method.primary). 99 | pub fn primary(&self) -> Result { 100 | let model = T::native_db_model(); 101 | let result = self.internal.primary_len(model)?; 102 | Ok(result) 103 | } 104 | 105 | /// Get the number of values by secondary key. 106 | /// 107 | /// Same as [`RLen::secondary()`](struct.RLen.html#method.secondary). 108 | pub fn secondary(&self, key_def: impl ToKeyDefinition) -> Result { 109 | let model = T::native_db_model(); 110 | let result = self.internal.secondary_len(model, key_def)?; 111 | Ok(result) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | set shell := ["nu", "-c"] 2 | 3 | default: 4 | @just --list --unsorted; 5 | 6 | build_no_default *args: 7 | cargo build --no-default-features {{args}} 8 | 9 | # E.g. just build_default --test modules breaking_release_migration::from_0_5_x_to_0_6_x 10 | build_default *args: 11 | cargo build {{args}} 12 | 13 | build_with_optional *args: 14 | cargo build -F tokio {{args}} 15 | 16 | build_examples *args: 17 | cd {{justfile_directory()}}/examples/major_upgrade; cargo build {{args}} 18 | 19 | build_all *args: 20 | just build_no_default {{args}}; 21 | just build_default {{args}}; 22 | just build_with_optional {{args}}; 23 | just build_examples {{args}}; 24 | 25 | test_no_default *args: 26 | cargo test --no-default-features {{args}} -- --nocapture 27 | 28 | test_default *args: 29 | cargo test {{args}} -- --nocapture 30 | 31 | test_with_optional *args: 32 | cargo test -F tokio {{args}} -- --nocapture 33 | 34 | test_examples *args: 35 | cd {{justfile_directory()}}/examples/major_upgrade; cargo test {{args}} -- --nocapture 36 | 37 | test_all *args: 38 | just test_no_default {{args}}; 39 | just test_default {{args}}; 40 | just test_with_optional {{args}}; 41 | just test_examples {{args}}; 42 | 43 | 44 | # List all available devices 45 | test_mobile_all_devices: 46 | cargo dinghy all-devices 47 | 48 | # List all available platforms 49 | test_mobile_all_platforms: 50 | echo $env.ANDROID_NDK_HOME; \ 51 | cargo dinghy all-platforms 52 | 53 | [macos] 54 | test_ios_launch_simulator device="iPhone 14": 55 | xcrun simctl boot "{{device}}" 56 | 57 | [macos] 58 | test_ios_list_simulators: 59 | xcrun simctl list 60 | 61 | # args: E.g. "--test modules watch::watch_multithreading" 62 | [macos] 63 | test_ios *args: 64 | cargo dinghy -d iphone test {{args}} 65 | 66 | # Run iOS tests excluding doctests (for CI - avoids cargo-dinghy doctest packaging issues) 67 | [macos] 68 | test_ios_lib *args: 69 | cargo dinghy -d iphone test --lib {{args}} 70 | 71 | # List all available android emulators 72 | test_android_list_emulators: 73 | emulator -list-avds 74 | 75 | # Launch android emulator 76 | test_android_launch_emulator emulator="Pixel_3a_API_34_extension_level_7_arm64-v8a": 77 | emulator -avd "{{emulator}}" 78 | 79 | # List all adb devices 80 | test_android_list_devices: 81 | adb devices 82 | 83 | test_android *args: 84 | cargo dinghy -d android test {{args}} 85 | 86 | # Run Android tests excluding doctests (for CI - avoids permission and "text file busy" issues) 87 | test_android_lib *args: 88 | cargo dinghy -d android test --lib {{args}} 89 | 90 | bench_build: 91 | cargo bench --no-run 92 | 93 | bench bench_name: 94 | CRITERION_DEBUG=1 cargo bench --profile release --bench {{bench_name}}; \ 95 | start ./target/criterion/report/index.html 96 | 97 | bench_md bench_name: 98 | cargo criterion --message-format=json --bench {{bench_name}} | save -f --raw ./benches/result.json; \ 99 | cat ./benches/result.json | criterion-table | save -f --raw ./benches/README.md 100 | 101 | bench_r_md: 102 | cat ./benches/result.json | criterion-table | save -f --raw ./benches/README.md 103 | 104 | expand test_file_name="util": 105 | rm -f {{test_file_name}}.expanded.rs; \ 106 | RUSTFLAGS="-Zmacro-backtrace" cargo expand --test {{test_file_name}} | save -f --raw src/{{test_file_name}}_expanded.rs 107 | 108 | expand_clean: 109 | rm -f src/*_expanded.rs 110 | 111 | format_examples: 112 | cd {{justfile_directory()}}/examples/major_upgrade; cargo fmt 113 | 114 | format: 115 | cargo clippy; \ 116 | cargo fmt --all; \ 117 | just format_examples 118 | 119 | fmt_check_examples: 120 | cd {{justfile_directory()}}/examples/major_upgrade; cargo fmt -- --check 121 | 122 | fmt_check: 123 | cargo fmt --all -- --check; \ 124 | just fmt_check_examples 125 | 126 | clippy_check_examples: 127 | cd {{justfile_directory()}}/examples/major_upgrade; cargo clippy -- -D warnings 128 | 129 | clippy_check: 130 | rustc --version; \ 131 | cargo clippy --version; \ 132 | cargo clippy -- -D warnings; \ 133 | just clippy_check_examples 134 | 135 | # Format check 136 | fc: 137 | just fmt_check; \ 138 | just clippy_check -------------------------------------------------------------------------------- /tests/transaction.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | use std::panic::AssertUnwindSafe; 6 | 7 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] 8 | #[native_model(id = 1, version = 1)] 9 | #[native_db] 10 | struct Item { 11 | #[primary_key] 12 | id: u32, 13 | name: String, 14 | } 15 | 16 | #[test] 17 | fn test_transaction_obj_1() { 18 | let tf = TmpFs::new().unwrap(); 19 | 20 | let mut models = Models::new(); 21 | models.define::().unwrap(); 22 | let db = Builder::new() 23 | .create(&models, tf.path("test").as_std_path()) 24 | .unwrap(); 25 | 26 | let item = Item { 27 | id: 1, 28 | name: "test".to_string(), 29 | }; 30 | 31 | let rw = db.rw_transaction().unwrap(); 32 | rw.insert(item).unwrap(); 33 | rw.commit().unwrap(); 34 | 35 | let r = db.r_transaction().unwrap(); 36 | let result: Item = r.get().primary(1u32).unwrap().unwrap(); 37 | assert_eq!(result.id, 1); 38 | } 39 | 40 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)] 41 | #[native_model(id = 2, version = 1)] 42 | #[native_db] 43 | struct Item2 { 44 | #[primary_key] 45 | id: u32, 46 | name: String, 47 | } 48 | 49 | #[test] 50 | fn test_transaction_obj_1_and_obj_2() { 51 | let tf = TmpFs::new().unwrap(); 52 | 53 | let mut models = Models::new(); 54 | models.define::().unwrap(); 55 | models.define::().unwrap(); 56 | let db = Builder::new() 57 | .create(&models, tf.path("test").as_std_path()) 58 | .unwrap(); 59 | 60 | let item_1 = Item { 61 | id: 1, 62 | name: "test".to_string(), 63 | }; 64 | let item_2 = Item2 { 65 | id: 2, 66 | name: "test".to_string(), 67 | }; 68 | 69 | let rw = db.rw_transaction().unwrap(); 70 | rw.insert(item_1).unwrap(); 71 | rw.insert(item_2).unwrap(); 72 | rw.commit().unwrap(); 73 | 74 | let r = db.r_transaction().unwrap(); 75 | let result: Item = r.get().primary(1u32).unwrap().unwrap(); 76 | assert_eq!(result.id, 1); 77 | let result: Item2 = r.get().primary(2u32).unwrap().unwrap(); 78 | assert_eq!(result.id, 2); 79 | } 80 | 81 | #[test] 82 | fn test_abort_transaction_obj_1_and_obj_2() { 83 | let tf = TmpFs::new().unwrap(); 84 | 85 | let mut models = Models::new(); 86 | models.define::().unwrap(); 87 | models.define::().unwrap(); 88 | let db = Builder::new() 89 | .create(&models, tf.path("test").as_std_path()) 90 | .unwrap(); 91 | 92 | let item_1 = Item { 93 | id: 1, 94 | name: "test".to_string(), 95 | }; 96 | 97 | let rw = db.rw_transaction().unwrap(); 98 | rw.insert(item_1).unwrap(); 99 | rw.abort().unwrap(); 100 | // After abort, the transaction, the transaction can not be used anymore. 101 | //rw.insert(item_2).unwrap(); 102 | //rw.commit().unwrap(); 103 | 104 | let r = db.r_transaction().unwrap(); 105 | assert!(r.get().primary::(1u32).unwrap().is_none()); 106 | } 107 | 108 | #[allow(unreachable_code)] 109 | #[test] 110 | fn test_transaction_fail() { 111 | let tf = TmpFs::new().unwrap(); 112 | 113 | let mut models = Models::new(); 114 | models.define::().unwrap(); 115 | let db = Builder::new() 116 | .create(&models, tf.path("test").as_std_path()) 117 | .unwrap(); 118 | 119 | let item_1 = Item { 120 | id: 1, 121 | name: "test".to_string(), 122 | }; 123 | 124 | let rw = db.rw_transaction().unwrap(); 125 | rw.insert(item_1).unwrap(); 126 | rw.commit().unwrap(); 127 | 128 | let r = db.r_transaction().unwrap(); 129 | let result: Item = r.get().primary(1u32).unwrap().unwrap(); 130 | assert_eq!(result.id, 1); 131 | 132 | let item_2 = Item { 133 | id: 2, 134 | name: "test".to_string(), 135 | }; 136 | let result = std::panic::catch_unwind(AssertUnwindSafe(|| { 137 | let rw = db.rw_transaction().unwrap(); 138 | rw.insert(item_2).unwrap(); 139 | panic!("Random panic here...") 140 | })); 141 | 142 | assert!(result.is_err()); 143 | 144 | let r = db.r_transaction().unwrap(); 145 | let result: Option = r.get().primary(2u32).unwrap(); 146 | assert!(result.is_none()); 147 | } 148 | -------------------------------------------------------------------------------- /src/transaction/query/scan/mod.rs: -------------------------------------------------------------------------------- 1 | mod primary_scan; 2 | mod secondary_scan; 3 | 4 | use crate::db_type::{Key, KeyOptions, Result, ToInput, ToKeyDefinition}; 5 | pub use primary_scan::*; 6 | pub use secondary_scan::*; 7 | 8 | use crate::transaction::internal::private_readable_transaction::PrivateReadableTransaction; 9 | use crate::transaction::internal::r_transaction::InternalRTransaction; 10 | use crate::transaction::internal::rw_transaction::InternalRwTransaction; 11 | 12 | /// Get values from the database. 13 | pub struct RScan<'db, 'txn> { 14 | pub(crate) internal: &'txn InternalRTransaction<'db>, 15 | } 16 | 17 | impl RScan<'_, '_> { 18 | /// Get a values from the database by primary key. 19 | /// 20 | /// - [`all`](crate::transaction::query::PrimaryScan::all) - Scan all items. 21 | /// - [`start_with`](crate::transaction::query::PrimaryScan::start_with) - Scan items with a primary key starting with a key. 22 | /// - [`range`](crate::transaction::query::PrimaryScan::range) - Scan items with a primary key in a given range. 23 | pub fn primary( 24 | &self, 25 | ) -> Result, T>> { 26 | let model = T::native_db_model(); 27 | let table = self.internal.get_primary_table(&model)?; 28 | let out = PrimaryScan::new(table); 29 | Ok(out) 30 | } 31 | 32 | #[allow(clippy::type_complexity)] 33 | /// Get a values from the database by secondary key. 34 | /// 35 | /// - [`all`](crate::transaction::query::SecondaryScan::all) - Scan all items. 36 | /// - [`start_with`](crate::transaction::query::SecondaryScan::start_with) - Scan items with a secondary key starting with a key. 37 | /// - [`range`](crate::transaction::query::SecondaryScan::range) - Scan items with a secondary key in a given range. 38 | pub fn secondary( 39 | &self, 40 | key_def: impl ToKeyDefinition, 41 | ) -> Result< 42 | SecondaryScan< 43 | redb::ReadOnlyTable, 44 | redb::ReadOnlyMultimapTable, 45 | T, 46 | >, 47 | > { 48 | let model = T::native_db_model(); 49 | let primary_table = self.internal.get_primary_table(&model)?; 50 | let secondary_key = key_def.key_definition(); 51 | let secondary_table = self.internal.get_secondary_table(&model, &secondary_key)?; 52 | let out = SecondaryScan::new(primary_table, secondary_table, key_def); 53 | Ok(out) 54 | } 55 | } 56 | 57 | pub struct RwScan<'db, 'txn> { 58 | pub(crate) internal: &'txn InternalRwTransaction<'db>, 59 | } 60 | 61 | impl<'db, 'txn> RwScan<'db, 'txn> 62 | where 63 | 'txn: 'db, 64 | { 65 | /// Get a values from the database by primary key. 66 | /// 67 | /// - [`all`](crate::transaction::query::PrimaryScan::all) - Scan all items. 68 | /// - [`start_with`](crate::transaction::query::PrimaryScan::start_with) - Scan items with a primary key starting with a key. 69 | /// - [`range`](crate::transaction::query::PrimaryScan::range) - Scan items with a primary key in a given range. 70 | pub fn primary( 71 | &self, 72 | ) -> Result, T>> { 73 | let model = T::native_db_model(); 74 | let table = self.internal.get_primary_table(&model)?; 75 | let out = PrimaryScan::new(table); 76 | Ok(out) 77 | } 78 | 79 | #[allow(clippy::type_complexity)] 80 | /// Get a values from the database by secondary key. 81 | /// 82 | /// - [`all`](crate::transaction::query::SecondaryScan::all) - Scan all items. 83 | /// - [`start_with`](crate::transaction::query::SecondaryScan::start_with) - Scan items with a secondary key starting with a key. 84 | /// - [`range`](crate::transaction::query::SecondaryScan::range) - Scan items with a secondary key in a given range. 85 | pub fn secondary( 86 | &self, 87 | key_def: impl ToKeyDefinition, 88 | ) -> Result< 89 | SecondaryScan, redb::MultimapTable<'db, Key, Key>, T>, 90 | > { 91 | let model = T::native_db_model(); 92 | let primary_table = self.internal.get_primary_table(&model)?; 93 | let secondary_key = key_def.key_definition(); 94 | let secondary_table = self.internal.get_secondary_table(&model, &secondary_key)?; 95 | let out = SecondaryScan::new(primary_table, secondary_table, key_def); 96 | Ok(out) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /examples/major_upgrade/src/new_main.rs: -------------------------------------------------------------------------------- 1 | use crate::models::current_version::CurrentModel; 2 | use crate::models::v08x::V08xModel; 3 | use native_db_current::{ 4 | upgrade::UpgradeResultExt, Builder as CurrentBuilder, Models as CurrentModels, 5 | }; 6 | use native_db_v0_8_x::{Builder as V08xBuilder, Models as V08xModels}; 7 | use std::path::Path; 8 | 9 | pub fn main>(db_path: P) -> Result<(), native_db_current::db_type::Error> { 10 | let db_path = db_path.as_ref(); 11 | 12 | // Define the models for current version 13 | let mut current_models = CurrentModels::new(); 14 | current_models.define::()?; 15 | 16 | // Try to open the database with current version 17 | match CurrentBuilder::new().open(¤t_models, db_path) { 18 | Ok(db) => { 19 | println!("Successfully opened database with current version"); 20 | 21 | // Verify we can read data 22 | let r = db.r_transaction()?; 23 | let count = r.len().primary::()?; 24 | println!("Database contains {count} items"); 25 | 26 | Ok(()) 27 | } 28 | Err(native_db_current::db_type::Error::UpgradeRequired(_)) => { 29 | println!("Database requires upgrade from v0.8.x to current version"); 30 | 31 | // Use the database upgrade method 32 | let upgraded_db = 33 | CurrentBuilder::new().upgrade(¤t_models, db_path, |new_txn| { 34 | // Open the old database inside the closure 35 | let mut old_models = V08xModels::new(); 36 | old_models 37 | .define::() 38 | .upgrade_context("defining old model")?; 39 | 40 | let old_db = V08xBuilder::new() 41 | .open(&old_models, db_path) 42 | .upgrade_context("opening old database")?; 43 | 44 | // Read all data from old database 45 | let old_txn = old_db 46 | .r_transaction() 47 | .upgrade_context("creating read transaction")?; 48 | let mut count = 0; 49 | 50 | // Use scan to iterate through all items 51 | let primary_scan = old_txn 52 | .scan() 53 | .primary() 54 | .upgrade_context("creating primary scan")?; 55 | 56 | let scan_iter = primary_scan 57 | .all() 58 | .upgrade_context("creating scan iterator")?; 59 | 60 | for item_result in scan_iter { 61 | let old_item: V08xModel = 62 | item_result.upgrade_context("reading item from old database")?; 63 | 64 | // Convert from old model to new model 65 | let new_item: CurrentModel = old_item.into(); 66 | 67 | // Insert into new database 68 | new_txn.insert(new_item)?; 69 | count += 1; 70 | } 71 | 72 | println!("Migrated {count} items"); 73 | 74 | // Old database automatically closes when it goes out of scope 75 | Ok(()) 76 | })?; 77 | 78 | println!("Successfully upgraded database to current version"); 79 | 80 | // Verify data was migrated 81 | let r = upgraded_db.r_transaction()?; 82 | let count = r.len().primary::()?; 83 | println!("Database contains {count} items after migration"); 84 | 85 | Ok(()) 86 | } 87 | Err(e) => { 88 | // If database doesn't exist or other error, create new 89 | if !db_path.exists() { 90 | println!("Creating new database with current version"); 91 | let db = CurrentBuilder::new().create(¤t_models, db_path)?; 92 | 93 | // Insert some test data 94 | let rw = db.rw_transaction()?; 95 | rw.insert(CurrentModel { 96 | id: 1, 97 | name: "Test Item Current".to_string(), 98 | })?; 99 | rw.insert(CurrentModel { 100 | id: 2, 101 | name: "Another Item Current".to_string(), 102 | })?; 103 | rw.commit()?; 104 | 105 | println!("Created new database with test data"); 106 | Ok(()) 107 | } else { 108 | Err(e) 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/query/insert_get_sk.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | 6 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 7 | #[native_model(id = 1, version = 1)] 8 | #[native_db( 9 | primary_key(pk -> String), 10 | secondary_key(gk_1 -> String, unique) 11 | )] 12 | struct Item { 13 | id: u32, 14 | name: String, 15 | } 16 | 17 | impl Item { 18 | pub fn pk(&self) -> String { 19 | format!("{}", self.id) 20 | } 21 | 22 | pub fn gk_1(&self) -> String { 23 | format!("{}-{}", self.name, self.id) 24 | } 25 | } 26 | 27 | #[test] 28 | fn insert_get_read_write_transaction() { 29 | let tf = TmpFs::new().unwrap(); 30 | 31 | let item = Item { 32 | id: 1, 33 | name: "test".to_string(), 34 | }; 35 | 36 | let mut models = Models::new(); 37 | models.define::().unwrap(); 38 | let db = Builder::new() 39 | .create(&models, tf.path("test").as_std_path()) 40 | .unwrap(); 41 | 42 | let rw = db.rw_transaction().unwrap(); 43 | rw.insert(item.clone()).unwrap(); 44 | rw.commit().unwrap(); 45 | 46 | let rw = db.rw_transaction().unwrap(); 47 | let result_item = rw 48 | .get() 49 | .secondary(ItemKey::gk_1, "test-1") 50 | .unwrap() 51 | .unwrap(); 52 | assert_eq!(item, result_item); 53 | rw.commit().unwrap(); 54 | } 55 | 56 | #[test] 57 | fn insert_get_read_transaction() { 58 | let tf = TmpFs::new().unwrap(); 59 | 60 | let item = Item { 61 | id: 1, 62 | name: "test".to_string(), 63 | }; 64 | 65 | let mut models = Models::new(); 66 | models.define::().unwrap(); 67 | let db = Builder::new() 68 | .create(&models, tf.path("test").as_std_path()) 69 | .unwrap(); 70 | 71 | let rw = db.rw_transaction().unwrap(); 72 | rw.insert(item.clone()).unwrap(); 73 | rw.commit().unwrap(); 74 | 75 | let r = db.r_transaction().unwrap(); 76 | let result_item = r.get().secondary(ItemKey::gk_1, "test-1").unwrap().unwrap(); 77 | 78 | assert_eq!(item, result_item); 79 | } 80 | 81 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 82 | #[native_model(id = 1, version = 1)] 83 | #[native_db] 84 | struct ItemDuplicate { 85 | #[primary_key] 86 | id: u32, 87 | #[secondary_key(unique)] 88 | name: String, 89 | } 90 | 91 | #[test] 92 | fn test_insert_duplicate_key() { 93 | let tf = TmpFs::new().unwrap(); 94 | 95 | let item_1 = ItemDuplicate { 96 | id: 1, 97 | name: "test".to_string(), 98 | }; 99 | let item_2 = ItemDuplicate { 100 | id: 2, 101 | name: "test".to_string(), 102 | }; 103 | 104 | let mut models = Models::new(); 105 | models.define::().unwrap(); 106 | let db = Builder::new() 107 | .create(&models, tf.path("test").as_std_path()) 108 | .unwrap(); 109 | 110 | let rw = db.rw_transaction().unwrap(); 111 | rw.insert(item_1).unwrap(); 112 | let result = rw.insert(item_2); 113 | assert!(result.is_err()); 114 | assert!(matches!( 115 | result.unwrap_err(), 116 | db_type::Error::DuplicateKey { .. } 117 | )); 118 | } 119 | 120 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 121 | #[native_model(id = 1, version = 1)] 122 | #[native_db] 123 | struct ItemOptional { 124 | #[primary_key] 125 | id: u32, 126 | #[secondary_key(unique, optional)] 127 | name: Option, 128 | } 129 | 130 | #[test] 131 | fn test_insert_optional() { 132 | let tf = TmpFs::new().unwrap(); 133 | 134 | let item_1 = ItemOptional { 135 | id: 1, 136 | name: Some("test".to_string()), 137 | }; 138 | let item_2 = ItemOptional { id: 2, name: None }; 139 | 140 | let mut models = Models::new(); 141 | models.define::().unwrap(); 142 | let db = Builder::new() 143 | .create(&models, tf.path("test").as_std_path()) 144 | .unwrap(); 145 | 146 | let rw = db.rw_transaction().unwrap(); 147 | rw.insert(item_1.clone()).unwrap(); 148 | rw.insert(item_2.clone()).unwrap(); 149 | rw.commit().unwrap(); 150 | 151 | let stats = db.redb_stats().unwrap(); 152 | assert_eq!(stats.primary_tables.len(), 1); 153 | assert_eq!(stats.primary_tables[0].name, "1_1_id"); 154 | assert_eq!(stats.primary_tables[0].n_entries, Some(2)); 155 | assert_eq!(stats.secondary_tables.len(), 1); 156 | assert_eq!(stats.secondary_tables[0].name, "1_1_name"); 157 | assert_eq!(stats.secondary_tables[0].n_entries, Some(1)); 158 | 159 | let r = db.r_transaction().unwrap(); 160 | let result_item = r 161 | .get() 162 | .secondary(ItemOptionalKey::name, Some("test")) 163 | .unwrap() 164 | .unwrap(); 165 | assert_eq!(item_1, result_item); 166 | } 167 | -------------------------------------------------------------------------------- /tests/macro_def/secondary_key.rs: -------------------------------------------------------------------------------- 1 | use native_db::db_type::{KeyDefinition, KeyEntry, ToInput}; 2 | use native_db::*; 3 | use native_model::{native_model, Model}; 4 | use serde::{Deserialize, Serialize}; 5 | use std::collections::HashMap; 6 | 7 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] 8 | #[native_model(id = 1, version = 1)] 9 | #[native_db( 10 | primary_key(compute_primary_key -> String), 11 | secondary_key(compute_secondary_key -> String), 12 | )] 13 | struct ItemSecondary { 14 | id: u32, 15 | name: String, 16 | } 17 | 18 | impl ItemSecondary { 19 | pub fn compute_primary_key(&self) -> String { 20 | format!("{}-{}", self.id, self.name) 21 | } 22 | pub fn compute_secondary_key(&self) -> String { 23 | format!("{}-{}", self.name, self.id) 24 | } 25 | } 26 | 27 | #[test] 28 | fn test_secondary() { 29 | let item = ItemSecondary { 30 | id: 1, 31 | name: "test".to_string(), 32 | }; 33 | 34 | let primary_key = item.native_db_primary_key(); 35 | assert_eq!(primary_key, "1-test".to_key()); 36 | 37 | let secondary_key: HashMap<_, KeyEntry> = item.native_db_secondary_keys(); 38 | assert_eq!(secondary_key.len(), 1); 39 | assert_eq!( 40 | secondary_key 41 | .get(&KeyDefinition::new( 42 | 1, 43 | 1, 44 | "compute_secondary_key", 45 | vec!["String".to_string()], 46 | Default::default() 47 | )) 48 | .unwrap(), 49 | &KeyEntry::Default("test-1".to_key()) 50 | ); 51 | } 52 | 53 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] 54 | #[native_model(id = 2, version = 1)] 55 | #[native_db( 56 | primary_key(compute_primary_key -> String), 57 | secondary_key(compute_secondary_key -> String, unique) 58 | )] 59 | struct ItemSecondaryUnique { 60 | id: u32, 61 | name: String, 62 | } 63 | 64 | impl ItemSecondaryUnique { 65 | pub fn compute_primary_key(&self) -> String { 66 | format!("{}-{}", self.id, self.name) 67 | } 68 | pub fn compute_secondary_key(&self) -> String { 69 | format!("{}-{}", self.name, self.id) 70 | } 71 | } 72 | 73 | #[test] 74 | fn test_secondary_unique() { 75 | let item = ItemSecondaryUnique { 76 | id: 1, 77 | name: "test".to_string(), 78 | }; 79 | 80 | let primary_key = item.native_db_primary_key(); 81 | assert_eq!(primary_key, "1-test".to_key()); 82 | 83 | let secondary_key: HashMap<_, KeyEntry> = item.native_db_secondary_keys(); 84 | assert_eq!(secondary_key.len(), 1); 85 | assert_eq!( 86 | secondary_key 87 | .get(&KeyDefinition::new( 88 | 2, 89 | 1, 90 | "compute_secondary_key", 91 | vec!["String".to_string()], 92 | Default::default() 93 | )) 94 | .unwrap(), 95 | &KeyEntry::Default("test-1".to_key()) 96 | ); 97 | } 98 | 99 | #[derive(Serialize, Deserialize, Eq, PartialEq, Debug)] 100 | #[native_model(id = 2, version = 1)] 101 | #[native_db( 102 | primary_key(compute_primary_key -> String), 103 | secondary_key(compute_secondary_key -> Option, optional) 104 | )] 105 | struct ItemSecondaryOptional { 106 | id: u32, 107 | name: Option, 108 | } 109 | 110 | impl ItemSecondaryOptional { 111 | pub fn compute_primary_key(&self) -> String { 112 | format!("{}", self.id) 113 | } 114 | pub fn compute_secondary_key(&self) -> Option { 115 | self.name 116 | .as_ref() 117 | .map(|name| format!("{}-{}", name, self.id)) 118 | } 119 | } 120 | 121 | #[test] 122 | fn test_secondary_optional() { 123 | let item = ItemSecondaryOptional { 124 | id: 1, 125 | name: Some("test".to_string()), 126 | }; 127 | 128 | let primary_key = item.native_db_primary_key(); 129 | assert_eq!(primary_key, "1".to_key()); 130 | 131 | let secondary_key: HashMap<_, KeyEntry> = item.native_db_secondary_keys(); 132 | assert_eq!(secondary_key.len(), 1); 133 | assert_eq!( 134 | secondary_key 135 | .get(&KeyDefinition::new( 136 | 2, 137 | 1, 138 | "compute_secondary_key", 139 | vec!["Option".to_string()], 140 | Default::default() 141 | )) 142 | .unwrap(), 143 | &KeyEntry::Optional(Some("test-1".to_key())) 144 | ); 145 | 146 | let item_none = ItemSecondaryOptional { id: 2, name: None }; 147 | let secondary_key: HashMap<_, KeyEntry> = item_none.native_db_secondary_keys(); 148 | assert_eq!(secondary_key.len(), 1); 149 | assert_eq!( 150 | secondary_key 151 | .get(&KeyDefinition::new( 152 | 2, 153 | 1, 154 | "compute_secondary_key", 155 | vec!["Option".to_string()], 156 | Default::default() 157 | )) 158 | .unwrap(), 159 | &KeyEntry::Optional(None) 160 | ); 161 | } 162 | -------------------------------------------------------------------------------- /tests/query/auto_update_sk.rs: -------------------------------------------------------------------------------- 1 | use native_db::*; 2 | use native_model::{native_model, Model}; 3 | use serde::{Deserialize, Serialize}; 4 | use shortcut_assert_fs::TmpFs; 5 | 6 | #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug)] 7 | #[native_model(id = 1, version = 1)] 8 | #[native_db] 9 | struct Item { 10 | #[primary_key] 11 | id: u32, 12 | #[secondary_key] 13 | name: String, 14 | #[secondary_key(unique)] 15 | code: String, 16 | } 17 | 18 | #[test] 19 | fn auto_update_non_existent() { 20 | let tf = TmpFs::new().unwrap(); 21 | let mut models = Models::new(); 22 | models.define::().unwrap(); 23 | let db = Builder::new() 24 | .create(&models, tf.path("test").as_std_path()) 25 | .unwrap(); 26 | 27 | let rw = db.rw_transaction().unwrap(); 28 | let result = rw.auto_update(Item { 29 | id: 1, 30 | name: "test".to_string(), 31 | code: "A1".to_string(), 32 | }); 33 | assert!(result.unwrap().is_none()); 34 | rw.commit().unwrap(); 35 | } 36 | 37 | #[test] 38 | fn auto_update_existing_check_secondary_keys() { 39 | let tf = TmpFs::new().unwrap(); 40 | let mut models = Models::new(); 41 | models.define::().unwrap(); 42 | let db = Builder::new() 43 | .create(&models, tf.path("test").as_std_path()) 44 | .unwrap(); 45 | 46 | let initial_item = Item { 47 | id: 1, 48 | name: "initial".to_string(), 49 | code: "A1".to_string(), 50 | }; 51 | let updated_item = Item { 52 | id: 1, 53 | name: "updated".to_string(), 54 | code: "B1".to_string(), 55 | }; 56 | 57 | // Insert initial item 58 | let rw = db.rw_transaction().unwrap(); 59 | rw.insert(initial_item.clone()).unwrap(); 60 | rw.commit().unwrap(); 61 | 62 | // Update the item 63 | let rw = db.rw_transaction().unwrap(); 64 | let old_value = rw.auto_update(updated_item.clone()).unwrap(); 65 | assert!(old_value.is_some()); 66 | assert_eq!(old_value.unwrap(), initial_item); 67 | 68 | // Verify primary key lookup 69 | let current: Item = rw.get().primary(1u32).unwrap().unwrap(); 70 | assert_eq!(current, updated_item); 71 | 72 | // Verify secondary key lookup (non-unique) 73 | let by_name: Vec = rw 74 | .scan() 75 | .secondary(ItemKey::name) 76 | .unwrap() 77 | .all() 78 | .unwrap() 79 | .collect::, _>>() 80 | .unwrap(); 81 | assert_eq!(by_name.len(), 1); 82 | assert_eq!(by_name[0], updated_item); 83 | 84 | // Verify secondary key lookup (unique) 85 | let by_code: Item = rw 86 | .get() 87 | .secondary(ItemKey::code, "B1".to_string()) 88 | .unwrap() 89 | .unwrap(); 90 | assert_eq!(by_code, updated_item); 91 | 92 | // Old secondary keys should not exist 93 | let old_by_code: Option = rw.get().secondary(ItemKey::code, "A1".to_string()).unwrap(); 94 | assert!(old_by_code.is_none()); 95 | 96 | rw.commit().unwrap(); 97 | } 98 | 99 | #[test] 100 | fn auto_update_multiple_with_secondary_keys() { 101 | let tf = TmpFs::new().unwrap(); 102 | let mut models = Models::new(); 103 | models.define::().unwrap(); 104 | let db = Builder::new() 105 | .create(&models, tf.path("test").as_std_path()) 106 | .unwrap(); 107 | 108 | // Insert multiple items 109 | let rw = db.rw_transaction().unwrap(); 110 | for i in 1..=3 { 111 | rw.insert(Item { 112 | id: i, 113 | name: format!("item{}", i), 114 | code: format!("A{}", i), 115 | }) 116 | .unwrap(); 117 | } 118 | rw.commit().unwrap(); 119 | 120 | // Update middle item 121 | let rw = db.rw_transaction().unwrap(); 122 | let old_value = rw 123 | .auto_update(Item { 124 | id: 2, 125 | name: "updated".to_string(), 126 | code: "B2".to_string(), 127 | }) 128 | .unwrap(); 129 | assert!(old_value.is_some()); 130 | assert_eq!( 131 | old_value.unwrap(), 132 | Item { 133 | id: 2, 134 | name: "item2".to_string(), 135 | code: "A2".to_string(), 136 | } 137 | ); 138 | 139 | // Verify all items by primary key 140 | let item1: Item = rw.get().primary(1u32).unwrap().unwrap(); 141 | assert_eq!(item1.name, "item1"); 142 | assert_eq!(item1.code, "A1"); 143 | 144 | let item2: Item = rw.get().primary(2u32).unwrap().unwrap(); 145 | assert_eq!(item2.name, "updated"); 146 | assert_eq!(item2.code, "B2"); 147 | 148 | let item3: Item = rw.get().primary(3u32).unwrap().unwrap(); 149 | assert_eq!(item3.name, "item3"); 150 | assert_eq!(item3.code, "A3"); 151 | 152 | // Verify secondary key updates 153 | let by_code2: Item = rw 154 | .get() 155 | .secondary(ItemKey::code, "B2".to_string()) 156 | .unwrap() 157 | .unwrap(); 158 | assert_eq!(by_code2, item2); 159 | 160 | // Old secondary key should not exist 161 | let old_by_code2: Option = rw.get().secondary(ItemKey::code, "A2".to_string()).unwrap(); 162 | assert!(old_by_code2.is_none()); 163 | 164 | rw.commit().unwrap(); 165 | } 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Native DB 2 | 3 | [![](https://github.com/vincent-herlemont/native_db/actions/workflows/build_test_linux.yml/badge.svg?branch=main&event=push)](https://github.com/vincent-herlemont/native_db/actions/workflows/build_test_linux.yml) 4 | [![](https://github.com/vincent-herlemont/native_db/actions/workflows/build_test_macos.yml/badge.svg?branch=main&event=push)](https://github.com/vincent-herlemont/native_db/actions/workflows/build_test_macos.yml) 5 | [![](https://github.com/vincent-herlemont/native_db/actions/workflows/build_test_windows.yml/badge.svg?branch=main&event=push)](https://github.com/vincent-herlemont/native_db/actions/workflows/build_test_windows.yml) 6 | [![](https://github.com/vincent-herlemont/native_db/actions/workflows/build_test_ios.yml/badge.svg?branch=main&event=push)](https://github.com/vincent-herlemont/native_db/actions/workflows/build_test_ios.yml) 7 | [![](https://github.com/vincent-herlemont/native_db/actions/workflows/build_test_android.yml/badge.svg?branch=main&event=push)](https://github.com/vincent-herlemont/native_db/actions/workflows/build_test_android.yml) 8 | 9 | 10 | [![Crates.io](https://img.shields.io/crates/v/native_db)](https://crates.io/crates/native_db) 11 | [![Documentation](https://docs.rs/native_db/badge.svg)](https://docs.rs/native_db) 12 | [![License](https://img.shields.io/crates/l/native_db)](LICENSE) 13 | 14 | Here's a drop-in, fast, embedded database for multi-platform apps (server, desktop, mobile). Sync Rust types effortlessly. Enjoy! 😌🍃. 15 | 16 | # Features 17 | 18 | - Simple API 🦀. 19 | - Support for **multiple indexes** (primary, secondary, unique, non-unique, optional). 20 | - Note: Optional secondary keys with `None` values cannot be queried using range syntax. See [documentation](https://docs.rs/native_db) for details. 21 | - Fast, see [`sqlite` vs `redb` vs `native_db`](./benches/README.md) benchmarks. 22 | - Transparent serialization/deserialization using [native_model](https://github.com/vincent-herlemont/native_model). You can use any serialization library you want (`bincode`, `postcard`, your own etc.). 23 | - Ensure query **type safety** to prevent unexpected results caused by selecting with an incorrect type. 24 | - **Automatic model migration** 🌟. 25 | - **Thread-safe** and fully **ACID-compliant** transactions provided by [redb](https://github.com/cberner/redb). 26 | - **Real-time** subscription with filters for `insert`, `update` and `delete` operations. 27 | - Compatible with all Rust types (`enum`, `struct`, `tuple` etc.). 28 | - **Hot snapshots**. 29 | 30 | # Installation 31 | 32 | Add this to your `Cargo.toml`: 33 | ```toml 34 | [dependencies] 35 | native_db = "0.8.1" 36 | native_model = "0.4.20" 37 | ``` 38 | 39 | # Status 40 | 41 | Active development. The API is not stable yet and may change in the future. 42 | 43 | # How to use? 44 | 45 | - [Documentation API](https://docs.rs/native_db/latest/native_db/#api) 46 | - [Quick Start](https://docs.rs/native_db/latest/native_db/#quick_start) 47 | - [Major Version Upgrade Guide](./examples/major_upgrade/README.md) 48 | - Full example with Tauri: [native_db_tauri_vanilla](https://github.com/vincent-herlemont/native_db_tauri_vanilla) 49 | 50 | # Projects using Native DB 51 | 52 | - [polly-scheduler](https://github.com/dongbin86/polly-scheduler) 53 | - [Oku](https://okubrowser.github.io) 54 | 55 | If you want to propose your project or company that uses Native DB, please open a PR. 56 | 57 | # Example 58 | 59 | ```rust 60 | use serde::{Deserialize, Serialize}; 61 | use native_db::*; 62 | use native_db::native_model::{native_model, Model}; 63 | use once_cell::sync::Lazy; 64 | 65 | #[derive(Serialize, Deserialize, PartialEq, Debug)] 66 | #[native_model(id = 1, version = 1)] 67 | #[native_db] 68 | struct Item { 69 | #[primary_key] 70 | id: u32, 71 | #[secondary_key] 72 | name: String, 73 | } 74 | 75 | // Define the models 76 | // The lifetime of the models needs to be longer or equal to the lifetime of the database. 77 | // In many cases, it is simpler to use a static variable but it is not mandatory. 78 | static MODELS: Lazy = Lazy::new(|| { 79 | let mut models = Models::new(); 80 | models.define::().unwrap(); 81 | models 82 | }); 83 | 84 | fn main() -> Result<(), db_type::Error> { 85 | // Create a database in memory 86 | let mut db = Builder::new().create_in_memory(&MODELS)?; 87 | 88 | // Insert data (open a read-write transaction) 89 | let rw = db.rw_transaction()?; 90 | rw.insert(Item { id: 1, name: "red".to_string() })?; 91 | rw.insert(Item { id: 2, name: "green".to_string() })?; 92 | rw.insert(Item { id: 3, name: "blue".to_string() })?; 93 | rw.commit()?; 94 | 95 | // Open a read-only transaction 96 | let r = db.r_transaction()?; 97 | // Retrieve data with id=3 98 | let retrieve_data: Item = r.get().primary(3_u32)?.unwrap(); 99 | println!("data id='3': {:?}", retrieve_data); 100 | // Iterate items with name starting with "red" 101 | for item in r.scan().secondary::(ItemKey::name)?.start_with("red")? { 102 | println!("data name=\"red\": {:?}", item); 103 | } 104 | 105 | // Remove data (open a read-write transaction) 106 | let rw = db.rw_transaction()?; 107 | rw.remove(retrieve_data)?; 108 | rw.commit()?; 109 | Ok(()) 110 | } 111 | ``` 112 | -------------------------------------------------------------------------------- /src/watch/query/internal.rs: -------------------------------------------------------------------------------- 1 | use crate::db_type::{Error, KeyOptions, KeyRange, Result, ToInput, ToKey, ToKeyDefinition}; 2 | use crate::watch; 3 | use crate::watch::{MpscReceiver, TableFilter}; 4 | use std::ops::RangeBounds; 5 | use std::sync::atomic::AtomicU64; 6 | use std::sync::{Arc, Mutex, RwLock}; 7 | 8 | pub(crate) struct InternalWatch<'db> { 9 | pub(crate) watchers: &'db Arc>, 10 | pub(crate) watchers_counter_id: &'db AtomicU64, 11 | } 12 | 13 | impl InternalWatch<'_> { 14 | fn watch_generic( 15 | &self, 16 | table_filter: watch::TableFilter, 17 | ) -> Result<(MpscReceiver, u64)> { 18 | #[cfg(not(feature = "tokio"))] 19 | let (event_sender, event_receiver) = std::sync::mpsc::channel(); 20 | #[cfg(feature = "tokio")] 21 | let (event_sender, event_receiver) = tokio::sync::mpsc::unbounded_channel(); 22 | let event_sender = Arc::new(Mutex::new(event_sender)); 23 | let id = self.generate_watcher_id()?; 24 | let mut watchers = self.watchers.write().unwrap(); 25 | watchers.add_sender(id, &table_filter, Arc::clone(&event_sender)); 26 | Ok((event_receiver, id)) 27 | } 28 | 29 | fn generate_watcher_id(&self) -> Result { 30 | let value = self 31 | .watchers_counter_id 32 | .fetch_add(1, std::sync::atomic::Ordering::SeqCst); 33 | if value == u64::MAX { 34 | Err(Error::MaxWatcherReached) 35 | } else { 36 | Ok(value) 37 | } 38 | } 39 | 40 | pub(crate) fn watch_primary( 41 | &self, 42 | key: impl ToKey, 43 | ) -> Result<(MpscReceiver, u64)> { 44 | let table_name = T::native_db_model().primary_key; 45 | let key = key.to_key(); 46 | let table_filter = 47 | TableFilter::new_primary(table_name.unique_table_name.clone(), Some(key)); 48 | self.watch_generic(table_filter) 49 | } 50 | 51 | pub(crate) fn watch_primary_all( 52 | &self, 53 | ) -> Result<(MpscReceiver, u64)> { 54 | let table_name = T::native_db_model().primary_key; 55 | let table_filter = TableFilter::new_primary(table_name.unique_table_name.clone(), None); 56 | self.watch_generic(table_filter) 57 | } 58 | 59 | pub(crate) fn watch_primary_start_with( 60 | &self, 61 | start_with: impl ToKey, 62 | ) -> Result<(MpscReceiver, u64)> { 63 | let table_name = T::native_db_model().primary_key; 64 | let start_with = start_with.to_key(); 65 | let table_filter = 66 | TableFilter::new_primary_start_with(table_name.unique_table_name.clone(), start_with); 67 | self.watch_generic(table_filter) 68 | } 69 | 70 | pub(crate) fn watch_primary_range>( 71 | &self, 72 | range: R, 73 | ) -> Result<(MpscReceiver, u64)> { 74 | let table_name = T::native_db_model().primary_key; 75 | let table_filter = TableFilter::new_primary_range( 76 | table_name.unique_table_name.clone(), 77 | KeyRange::new(range), 78 | ); 79 | self.watch_generic(table_filter) 80 | } 81 | 82 | pub(crate) fn watch_secondary( 83 | &self, 84 | key_def: &impl ToKeyDefinition, 85 | key: impl ToKey, 86 | ) -> Result<(MpscReceiver, u64)> { 87 | let table_name = T::native_db_model().primary_key; 88 | let key = key.to_key(); 89 | let table_filter = 90 | TableFilter::new_secondary(table_name.unique_table_name.clone(), key_def, Some(key)); 91 | self.watch_generic(table_filter) 92 | } 93 | 94 | pub(crate) fn watch_secondary_all( 95 | &self, 96 | key_def: &impl ToKeyDefinition, 97 | ) -> Result<(MpscReceiver, u64)> { 98 | let table_name = T::native_db_model().primary_key; 99 | let table_filter = 100 | TableFilter::new_secondary(table_name.unique_table_name.clone(), key_def, None); 101 | self.watch_generic(table_filter) 102 | } 103 | 104 | pub(crate) fn watch_secondary_start_with( 105 | &self, 106 | key_def: &impl ToKeyDefinition, 107 | start_with: impl ToKey, 108 | ) -> Result<(MpscReceiver, u64)> { 109 | let table_name = T::native_db_model().primary_key; 110 | let start_with = start_with.to_key(); 111 | let table_filter = TableFilter::new_secondary_start_with( 112 | table_name.unique_table_name.clone(), 113 | key_def, 114 | start_with, 115 | ); 116 | self.watch_generic(table_filter) 117 | } 118 | 119 | pub(crate) fn watch_secondary_range>( 120 | &self, 121 | key_def: &impl ToKeyDefinition, 122 | range: R, 123 | ) -> Result<(MpscReceiver, u64)> { 124 | let table_name = T::native_db_model().primary_key; 125 | let table_filter = TableFilter::new_secondary_range( 126 | table_name.unique_table_name.clone(), 127 | key_def, 128 | KeyRange::new(range), 129 | ); 130 | self.watch_generic(table_filter) 131 | } 132 | } 133 | --------------------------------------------------------------------------------