diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..47fef75 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,58 @@ +name: Test and Build +on: + push: + branches: + - main + pull_request: + branches: + - main +env: + FORCE_COLOR: 1 + CARGO_TERM_COLOR: always + RUST_BACKTRACE: full +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Setup 3-node Scylla cluster + run: | + sudo sh -c "echo 2097152 >> /proc/sys/fs/aio-max-nr" + docker compose -f test/docker-compose.yml up -d --wait + - name: Create Keyspace + run: | + docker exec scylla1 cqlsh -e \ + "CREATE KEYSPACE charybdis WITH replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': 2};" + - name: Cache Dependencies + uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: Update rust toolchain + run: rustup update + - name: Print rustc version + run: rustc --version + - name: Print rustfmt version + run: cargo fmt --version + - name: Print clippy version + run: cargo clippy --version + - name: Format check + run: cargo fmt --verbose -- --check + - name: Clippy check + run: cargo clippy --verbose + - name: Cargo check + run: cargo check --verbose + - name: Build + run: cargo build --verbose + - name: Install Charybdis Migration Tool + run: cargo install --path charybdis-migrate --force + - name: Run Charybdis Migration Tool + run: migrate --keyspace charybdis --host 127.0.0.1:9042 --drop-and-replace + - name: Run tests + run: cargo test --verbose diff --git a/Cargo.lock b/Cargo.lock index f7b7be0..98e452b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,33 +212,33 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "charybdis" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2a25dc45c6f68710903d18635ddd348a6b67ada9cd9c9a8f36bb2d242d129a" +version = "0.7.10" dependencies = [ "bigdecimal", - "charybdis_macros 0.7.2", + "charybdis-migrate", + "charybdis_macros 0.7.10", "chrono", "colored", "futures", - "num-bigint 0.4.4", - "scylla 0.13.1", + "scylla", "serde", "serde_json", + "tokio", "uuid", ] [[package]] name = "charybdis" version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5bd195a7d47dc7aaa54ed6267320188aa8919ab222efdfeec2807755a311354" dependencies = [ "bigdecimal", - "charybdis-migrate", - "charybdis_macros 0.7.10", + "charybdis_macros 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)", "chrono", "colored", "futures", - "scylla 0.15.1", + "scylla", "serde", "serde_json", "uuid", @@ -253,47 +253,45 @@ dependencies = [ "colored", "openssl", "regex", - "scylla 0.15.1", + "scylla", "strip-ansi-escapes", "tokio", ] [[package]] name = "charybdis_macros" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22275746377c3263a9154d22c19a08498e8bcf511407175cb20bc3de8b055646" +version = "0.7.10" dependencies = [ - "charybdis_parser 0.7.2", - "darling", + "charybdis 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)", + "charybdis_parser 0.7.10", + "chrono", "proc-macro2", "quote", + "scylla", "syn", ] [[package]] name = "charybdis_macros" version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cacdb3cc06a8f0632281196559491343342f1d10417f17b3a8199623fbedf31" dependencies = [ - "charybdis 0.7.2", - "charybdis_parser 0.7.10", + "charybdis_parser 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)", "proc-macro2", "quote", - "scylla 0.15.1", "syn", ] [[package]] name = "charybdis_parser" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80a6cb30fe0040056e65e175ce03ac7e9d9ffde308ba95ced1be938bbb170dbb" +version = "0.7.10" dependencies = [ "colored", "darling", "proc-macro2", "quote", - "scylla 0.13.1", + "scylla", "serde", "serde_json", "strum", @@ -305,12 +303,14 @@ dependencies = [ [[package]] name = "charybdis_parser" version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f0f1b8d925e5f82d58fd9abfa5137693ac3474c5e5840c1f1c832511a23ec56" dependencies = [ "colored", "darling", "proc-macro2", "quote", - "scylla 0.15.1", + "scylla", "serde", "serde_json", "strum", @@ -608,12 +608,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "histogram" version = "0.6.9" @@ -649,15 +643,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -690,9 +675,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.152" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libm" @@ -742,13 +727,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.10" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -792,16 +777,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "object" version = "0.32.2" @@ -813,9 +788,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "openssl" @@ -1036,37 +1011,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scylla" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b20b46cf4ea921ba41121ba9ddf933185cd830cbe2c4fa6272a6e274a6b7368d" -dependencies = [ - "arc-swap", - "async-trait", - "byteorder", - "bytes", - "chrono", - "dashmap", - "futures", - "hashbrown", - "histogram", - "itertools 0.11.0", - "lazy_static", - "lz4_flex", - "rand", - "rand_pcg", - "scylla-cql 0.2.1", - "scylla-macros 0.5.1", - "smallvec", - "snap", - "socket2", - "thiserror 1.0.56", - "tokio", - "tracing", - "uuid", -] - [[package]] name = "scylla" version = "0.15.1" @@ -1082,47 +1026,24 @@ dependencies = [ "futures", "hashbrown", "histogram", - "itertools 0.13.0", + "itertools", "lazy_static", "lz4_flex", "openssl", "rand", "rand_pcg", - "scylla-cql 0.4.1", - "scylla-macros 0.7.1", + "scylla-cql", + "scylla-macros", "smallvec", "snap", "socket2", - "thiserror 2.0.7", + "thiserror", "tokio", "tokio-openssl", "tracing", "uuid", ] -[[package]] -name = "scylla-cql" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ea3cd3ff5bf9d7db7a6d65c54cecf52f7c40b8e3e32c8c2d6da84d23776ea4" -dependencies = [ - "async-trait", - "bigdecimal", - "byteorder", - "bytes", - "chrono", - "lz4_flex", - "num-bigint 0.3.3", - "num-bigint 0.4.4", - "scylla-macros 0.5.1", - "secrecy 0.7.0", - "snap", - "thiserror 1.0.56", - "time", - "tokio", - "uuid", -] - [[package]] name = "scylla-cql" version = "0.4.1" @@ -1137,29 +1058,17 @@ dependencies = [ "lz4_flex", "num-bigint 0.3.3", "num-bigint 0.4.4", - "scylla-macros 0.7.1", - "secrecy 0.8.0", + "scylla-macros", + "secrecy", "snap", "stable_deref_trait", - "thiserror 2.0.7", + "thiserror", "time", "tokio", "uuid", "yoke", ] -[[package]] -name = "scylla-macros" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e50f3e2aec7ea9f495e029fb783eb34c64d26a8f2055e1d6b43d00e04d2fbda6" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "scylla-macros" version = "0.7.1" @@ -1172,15 +1081,6 @@ dependencies = [ "syn", ] -[[package]] -name = "secrecy" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0673d6a6449f5e7d12a1caf424fd9363e2af3a4953023ed455e3c4beef4597c0" -dependencies = [ - "zeroize", -] - [[package]] name = "secrecy" version = "0.8.0" @@ -1329,33 +1229,13 @@ dependencies = [ "syn", ] -[[package]] -name = "thiserror" -version = "1.0.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" -dependencies = [ - "thiserror-impl 1.0.56", -] - [[package]] name = "thiserror" version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" dependencies = [ - "thiserror-impl 2.0.7", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -1389,28 +1269,27 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "tokio" -version = "1.38.0" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", diff --git a/charybdis-macros/Cargo.toml b/charybdis-macros/Cargo.toml index efc816d..a62f9a0 100644 --- a/charybdis-macros/Cargo.toml +++ b/charybdis-macros/Cargo.toml @@ -20,3 +20,4 @@ quote = "1.0.36" [dev-dependencies] charybdis = "0.7.2" scylla = "0.15.1" +chrono = "0.4.38" \ No newline at end of file diff --git a/charybdis-macros/src/model/consts/mod.rs b/charybdis-macros/src/model/consts/mod.rs index 9f026dd..d4d1ff8 100644 --- a/charybdis-macros/src/model/consts/mod.rs +++ b/charybdis-macros/src/model/consts/mod.rs @@ -10,4 +10,3 @@ mod insert; mod model_name; mod update; - diff --git a/charybdis-macros/src/native/delete.rs b/charybdis-macros/src/native/delete.rs index 1db7725..426e410 100644 --- a/charybdis-macros/src/native/delete.rs +++ b/charybdis-macros/src/native/delete.rs @@ -11,18 +11,37 @@ const MAX_DELETE_BY_FUNCTIONS: usize = 3; /// for 3 keys generate additional function that deletes by partition key & partial clustering key /// Example: -/// ```ignore +/// ```rust +/// use charybdis::macros::charybdis_model; +/// use charybdis::errors::CharybdisError; +/// use charybdis::types::{Timestamp, Uuid}; +/// use scylla::CachingSession; +/// /// #[charybdis_model( /// table_name = users, /// partition_keys = [id], /// clustering_keys = [org_id, created_at], -/// global_secondary_indexes = [])] -/// pub struct UserOps {...} +/// global_secondary_indexes = [] +/// )] +/// pub struct User { +/// id: Uuid, +/// org_id: Uuid, +/// created_at: Timestamp, +/// } +/// impl User { +/// pub async fn delete_funs(db: &CachingSession) -> Result<(), CharybdisError> { +/// let id = Uuid::new_v4(); +/// let org_id = Uuid::new_v4(); +/// let created_at = chrono::Utc::now(); +/// +/// User::delete_by_id(id).execute(db).await?; +/// User::delete_by_id_and_org_id(id, org_id).execute(db).await?; +/// User::delete_by_id_and_org_id_and_created_at(id, org_id, created_at).execute(db).await?; +/// +/// Ok(()) +/// } +/// } /// ``` -/// we would have a functions: -/// ```ignore -/// User::delete_by_id_and_org_id(session: &Session, org_id: Uuid) -> Result, errors::CharybdisError> -/// User::delete_by_id_and_org_id_and_created_at(session: &Session, org_id: Uuid, created_at: Timestamp) -> Result, errors::CharybdisError> pub(crate) fn delete_by_primary_key_functions(ch_args: &CharybdisMacroArgs, fields: &CharybdisFields) -> TokenStream { let table_name = ch_args.table_name(); let partition_keys_len = fields.partition_key_fields.len(); diff --git a/charybdis-macros/src/native/mod.rs b/charybdis-macros/src/native/mod.rs index d702337..fb032f6 100644 --- a/charybdis-macros/src/native/mod.rs +++ b/charybdis-macros/src/native/mod.rs @@ -8,4 +8,3 @@ mod counter; mod delete; mod find; - diff --git a/charybdis-macros/src/rules/delete.rs b/charybdis-macros/src/rules/delete.rs index cfe6f18..3b1b2b7 100644 --- a/charybdis-macros/src/rules/delete.rs +++ b/charybdis-macros/src/rules/delete.rs @@ -2,8 +2,8 @@ use proc_macro2::{Ident, TokenStream}; use quote::quote; use syn::parse_str; -use charybdis_parser::traits::CharybdisMacroArgs; use charybdis_parser::traits::string::ToSnakeCase; +use charybdis_parser::traits::CharybdisMacroArgs; pub(crate) fn delete_model_query_rule(struct_name: &Ident, args: &CharybdisMacroArgs) -> TokenStream { let macro_name_str: String = format!("delete_{}_query", struct_name.to_string().to_snake_case()); diff --git a/charybdis-macros/src/rules/find.rs b/charybdis-macros/src/rules/find.rs index ffc9976..e7569e8 100644 --- a/charybdis-macros/src/rules/find.rs +++ b/charybdis-macros/src/rules/find.rs @@ -3,8 +3,8 @@ use quote::quote; use syn::parse_str; use charybdis_parser::fields::CharybdisFields; -use charybdis_parser::traits::CharybdisMacroArgs; use charybdis_parser::traits::string::ToSnakeCase; +use charybdis_parser::traits::CharybdisMacroArgs; use crate::traits::fields::FieldsQuery; diff --git a/charybdis-macros/src/rules/mod.rs b/charybdis-macros/src/rules/mod.rs index 911ac10..fcec776 100644 --- a/charybdis-macros/src/rules/mod.rs +++ b/charybdis-macros/src/rules/mod.rs @@ -7,4 +7,3 @@ mod delete; mod find; mod partial; mod update; - diff --git a/charybdis-macros/src/rules/partial.rs b/charybdis-macros/src/rules/partial.rs index 7a1ac13..6460020 100644 --- a/charybdis-macros/src/rules/partial.rs +++ b/charybdis-macros/src/rules/partial.rs @@ -16,13 +16,19 @@ use crate::traits::fields::{FieldHashMapString, ToIdents}; /// It is basically the same as base model struct, but with provided fields only. /// /// So, if we have a model User with some fields: -/// ```ignore -/// use charybdis::*; -/// use super::Address; +/// Example: +/// ```rust +/// use charybdis::macros::charybdis_model; +/// use charybdis::types::{Text, Timestamp, Uuid}; +/// use scylla::CachingSession; +/// /// #[charybdis_model( -/// table_name = "users", -/// partition_keys = ["id"], -/// clustering_keys = [])] +/// table_name = users, +/// partition_keys = [id], +/// clustering_keys = [], +/// global_secondary_indexes = [] +/// )] +/// #[derive(Default)] /// pub struct User { /// pub id: Uuid, /// pub username: Text, @@ -31,92 +37,28 @@ use crate::traits::fields::{FieldHashMapString, ToIdents}; /// pub email: Text, /// pub created_at: Timestamp, /// pub updated_at: Timestamp, -/// pub address: Address, /// } +/// partial_user!(UpdateUsernameUser, id, username, updated_at); /// ``` -/// It will generate a `partial_user!` macro that can be used to generate a partial users structs. -/// -/// ## Example generation: -/// ```ignore -/// partial_user!(PartialUser, id, username, email); -/// ``` -/// It will generate a struct with only those fields: -/// ```ignore -/// #[charybdis_model( -/// table_name = "users", -/// partition_keys = ["id"], -/// clustering_keys = [] -/// global_secondary_indexes = [])] -/// pub struct PartialUser { -/// pub id: Uuid, -/// pub username: Text, -/// pub email: Text, -/// } -///``` -/// And we can use it as a normal model struct. +/// it will generate a struct with `#[charybdis_model(...)]` declaration like this: /// -/// ```ignore -/// let mut partial_user = PartialUser {id, username, email}; -/// partial_user.insert().execute(&session).await?; -/// partial_user.find_by_primary_key().execute(&session).await?; /// ``` -/// -/// ## Example usage: -/// ```ignore -/// partial_user!(PartialUser, id, username, email); -/// -/// let mut partial_user = PartialUser { -/// id: Uuid::new_v4(), -/// username: "test".to_string(), -/// email: "test@gmail.com".to_string(), -/// }; -/// -/// println!("{:?}", partial_user); -///``` -///--- -/// -/// ### `#[charybdis_model]` declaration -/// It also appends `#[charybdis_model(...)]` declaration with clustering keys and secondary indexes -/// based on fields that are provided in partial_model struct. -/// -/// E.g. if we have model: -/// ```ignore -/// #[partial_model_generator] +/// use charybdis::macros::charybdis_model; +/// use charybdis::types::{Text, Timestamp, Uuid}; +/// use scylla::CachingSession; /// #[charybdis_model( /// table_name = users, /// partition_keys = [id], -/// clustering_keys = [created_at, updated_at], +/// clustering_keys = [], /// global_secondary_indexes = [] /// )] -/// pub struct User { +/// pub struct UpdateUsernameUser { /// pub id: Uuid, /// pub username: Text, -/// pub password: Text, -/// pub hashed_password: Text, -/// pub email: Text, -/// pub created_at: Timestamp, /// pub updated_at: Timestamp, /// } /// ``` -/// -/// and we use partial model macro: -/// ```ignore -/// partial_user!(UserOps, id, username, email, created_at); -/// ``` -/// it will generate a struct with `#[charybdis_model(...)]` declaration: -/// -/// ```ignore -/// #[charybdis_model( -/// table_name = users, -/// partition_keys = [id], -/// clustering_keys = [created_at], -/// global_secondary_indexes = [])] -/// pub struct UserOps {...} -/// ``` -/// Note that `updated_at` is not present in generated declaration. -/// However, all partition keys are required for db operations, so we can't have partial partition -/// keys. -/// +/// Note that partial_user! requires `Default` to be implemented for the model. pub(crate) fn partial_model_macro_generator( input: &DeriveInput, args: &CharybdisMacroArgs, diff --git a/charybdis-macros/src/rules/update.rs b/charybdis-macros/src/rules/update.rs index 174b8a5..d07d4aa 100644 --- a/charybdis-macros/src/rules/update.rs +++ b/charybdis-macros/src/rules/update.rs @@ -3,8 +3,8 @@ use quote::quote; use syn::parse_str; use charybdis_parser::fields::CharybdisFields; -use charybdis_parser::traits::CharybdisMacroArgs; use charybdis_parser::traits::string::ToSnakeCase; +use charybdis_parser::traits::CharybdisMacroArgs; use crate::traits::fields::FieldsQuery; diff --git a/charybdis-macros/src/traits/fields/hash_map.rs b/charybdis-macros/src/traits/fields/hash_map.rs index 2ba9199..16ca188 100644 --- a/charybdis-macros/src/traits/fields/hash_map.rs +++ b/charybdis-macros/src/traits/fields/hash_map.rs @@ -41,4 +41,3 @@ impl FieldHashMapString for Vec> { field_attributes.to_string().replace('\n', "") } } - diff --git a/charybdis-migrate/src/model/data.rs b/charybdis-migrate/src/model/data.rs index a503168..225e3c9 100644 --- a/charybdis-migrate/src/model/data.rs +++ b/charybdis-migrate/src/model/data.rs @@ -1,7 +1,7 @@ use charybdis_parser::schema::{IndexName, SchemaObject}; -use crate::model::ModelType; use crate::model::runner::INDEX_SUFFIX; +use crate::model::ModelType; type FieldName = String; type FieldType = String; diff --git a/charybdis/Cargo.toml b/charybdis/Cargo.toml index 1b746d8..9f2ce48 100644 --- a/charybdis/Cargo.toml +++ b/charybdis/Cargo.toml @@ -24,3 +24,6 @@ bigdecimal = { version = "0.4.3", features = ["serde"] } [features] migrate = ["charybdis-migrate"] + +[dev-dependencies] +tokio = "1.42.0" diff --git a/charybdis/README.md b/charybdis/README.md index fce38f4..d28314a 100644 --- a/charybdis/README.md +++ b/charybdis/README.md @@ -7,7 +7,7 @@ welcomed! [![License](https://img.shields.io/crates/l/charybdis)]() [![Docs.rs](https://docs.rs/charybdis/badge.svg)](https://docs.rs/charybdis) [![Discord](https://img.shields.io/discord/1247167793045176461?label=discord-server)](https://discord.gg/enDd57nNen) - +![Build](https://github.com/nodecosmos/charybdis/actions/workflows/build.yml/badge.svg)

scylla_logo cassandra_logo diff --git a/charybdis/src/model.rs b/charybdis/src/model.rs index 3fe2c94..30ad5d2 100644 --- a/charybdis/src/model.rs +++ b/charybdis/src/model.rs @@ -116,10 +116,8 @@ pub trait TableOptions { const TABLE_OPTIONS: &'static str; } -/// -/// In extension of partial_model!() in case you need native model in order to run calculations -/// or other operations, you can use `as_native` method: -/// ```rust ignore +/// In extension of partial_model!() in case you need to build native model you can use `as_native` method: +/// ```rust /// use charybdis::macros::charybdis_model; /// use charybdis::types::{Text, Timestamp, Uuid}; /// use charybdis::model::AsNative; @@ -145,18 +143,12 @@ pub trait TableOptions { /// /// partial_user!(UpdateUsernameUser, id, username); /// -/// let mut user = UpdateUsernameUser { -/// id: Uuid::new_v4(), -/// username: "updated_username".to_string(), -/// }; -/// -/// let native_user: User = user.as_native(); -/// -/// // action that requires native model -/// // authorize_user(&native_user); +/// impl UpdateUsernameUser { +/// pub fn native(&self) -> User { +/// self.as_native() +/// } +/// } /// ``` -/// Its automatically generated by `#[partial_model_generator]`. -/// pub trait AsNative { fn as_native(&self) -> T; } diff --git a/charybdis/src/operations.rs b/charybdis/src/operations.rs index edab179..fcc56ff 100644 --- a/charybdis/src/operations.rs +++ b/charybdis/src/operations.rs @@ -11,4 +11,3 @@ mod find; mod insert; mod new; mod update; - diff --git a/charybdis/tests/integrations/common.rs b/charybdis/tests/integrations/common.rs new file mode 100644 index 0000000..143657e --- /dev/null +++ b/charybdis/tests/integrations/common.rs @@ -0,0 +1,15 @@ +use scylla::{CachingSession, SessionBuilder}; + +const NODES: [&str; 3] = ["127.0.0.1:9042", "127.0.0.1:9043", "127.0.0.1:9044"]; +const KEYSPACE: &str = "charybdis"; +const CACHE_SIZE: usize = 1000; + +pub async fn db_session() -> CachingSession { + let db_session = SessionBuilder::new() + .known_nodes(NODES) + .use_keyspace(KEYSPACE, false) + .build() + .await + .expect("Unable to connect to scylla hosts"); + CachingSession::from(db_session, CACHE_SIZE) +} diff --git a/charybdis/tests/integrations/main.rs b/charybdis/tests/integrations/main.rs new file mode 100644 index 0000000..9e3d6b7 --- /dev/null +++ b/charybdis/tests/integrations/main.rs @@ -0,0 +1,3 @@ +mod common; +mod model; +mod query; diff --git a/charybdis/tests/integrations/model.rs b/charybdis/tests/integrations/model.rs new file mode 100644 index 0000000..d6b23e8 --- /dev/null +++ b/charybdis/tests/integrations/model.rs @@ -0,0 +1,293 @@ +use crate::common::db_session; +use charybdis::batch::ModelBatch; +use charybdis::errors::CharybdisError; +use charybdis::model::{BaseModel, Model}; +use charybdis::stream::CharybdisModelStream; +use charybdis::types::{Boolean, Int, Text, Uuid}; +use charybdis_macros::{charybdis_model, charybdis_udt_model, charybdis_view_model}; + +#[derive(Debug, Default, Clone, PartialEq)] +#[charybdis_udt_model(type_name = address)] +pub struct Address { + pub street: Text, + pub city: Text, + pub state: Text, + pub zip: Text, + pub country: Text, +} + +#[charybdis_model( + table_name = users, + partition_keys = [id], + clustering_keys = [], + global_secondary_indexes = [username], +)] +#[derive(Debug, Default, Clone, PartialEq)] +pub struct User { + pub id: Uuid, + pub username: Text, + pub password: Text, + pub email: Text, + pub first_name: Text, + pub last_name: Text, + pub bio: Option, + pub address: Option

, + pub is_confirmed: Boolean, +} + +partial_user!(UpdateUsernameUser, id, username); + +impl User { + pub async fn populate_sample_users() { + let db_session = db_session().await; + let users = (0..32) + .map(|i| { + let id = Uuid::new_v4(); + let mut new_user = User::homer(id); + new_user.username = format!("user_{}", i); + new_user.email = format!("user_{}@gmail.com", i); + + new_user + }) + .collect::>(); + + User::batch() + .chunked_insert(&db_session, &users, 100) + .await + .expect("Failed to insert users"); + } + + pub fn homer(id: Uuid) -> Self { + User { + id, + username: "test".to_string(), + password: "Marge".to_string(), + first_name: "Homer".to_string(), + email: "homer@simpson.com".to_string(), + last_name: "Simpson".to_string(), + bio: Some("I like donuts".to_string()), + address: Some(Address { + street: "742 Evergreen Terrace".to_string(), + city: "Springfield".to_string(), + state: "Illinois".to_string(), + zip: "62701".to_string(), + country: "USA".to_string(), + }), + is_confirmed: true, + } + } +} + +#[tokio::test] +async fn user_model_queries() { + assert_eq!(User::DB_MODEL_NAME, "users"); + assert_eq!( + User::FIND_ALL_QUERY, + "SELECT id, username, password, email, first_name, last_name, bio, address, is_confirmed FROM users" + ); + assert_eq!( + User::FIND_BY_PRIMARY_KEY_QUERY, + "SELECT id, username, password, email, \ + first_name, last_name, bio, address, is_confirmed FROM users WHERE id = ?" + ); + assert_eq!( + User::FIND_BY_PARTITION_KEY_QUERY, + "SELECT id, username, password, email, \ + first_name, last_name, bio, address, is_confirmed FROM users WHERE id = ?" + ); + assert_eq!( + User::FIND_FIRST_BY_PARTITION_KEY_QUERY, + "SELECT id, username, password, email, \ + first_name, last_name, bio, address, is_confirmed FROM users WHERE id = ? LIMIT 1" + ); + assert_eq!( + User::INSERT_QUERY, + "INSERT INTO users (id, username, password, email, first_name, last_name, bio, address, is_confirmed) \ + VALUES (:id, :username, :password, :email, :first_name, :last_name, :bio, :address, :is_confirmed)" + ); + assert_eq!( + User::INSERT_IF_NOT_EXIST_QUERY, + "INSERT INTO users (id, username, password, email, first_name, last_name, bio, address, is_confirmed) \ + VALUES (:id, :username, :password, :email, :first_name, :last_name, :bio, :address, :is_confirmed) \ + IF NOT EXISTS" + ); + assert_eq!( + User::UPDATE_QUERY, + "UPDATE users SET username = :username, password = :password, email = :email, first_name = :first_name, \ + last_name = :last_name, bio = :bio, address = :address, is_confirmed = :is_confirmed WHERE id = :id" + ); + assert_eq!(User::DELETE_QUERY, "DELETE FROM users WHERE id = ?"); + assert_eq!(User::DELETE_BY_PARTITION_KEY_QUERY, "DELETE FROM users WHERE id = ?"); +} + +#[charybdis_view_model( + table_name=user_by_email, + base_table=users, + partition_keys=[email], + clustering_keys=[id] +)] +pub struct UserByEmail { + pub id: Uuid, + pub email: Text, + pub username: Text, +} + +#[charybdis_model( + table_name = posts, + partition_keys = [category_id], + clustering_keys = [order_idx, title], + global_secondary_indexes = [author_id], + local_secondary_indexes = [title], +)] +#[derive(Debug, Default, Clone, PartialEq)] +pub struct Post { + pub category_id: Uuid, + pub order_idx: Int, + pub title: Text, + pub content: Text, + pub author_id: Uuid, +} + +impl Post { + pub async fn populate_sample_posts_per_partition(category_id: Uuid) { + let db_session = db_session().await; + let posts = (0..32) + .map(|i| Post { + category_id, + order_idx: i, + title: format!("Post {}", i), + content: "Lorem ipsum dolor sit amet".to_string(), + author_id: Uuid::new_v4(), + }) + .collect::>(); + + Post::batch() + .chunked_insert(&db_session, &posts, 100) + .await + .expect("Failed to insert posts"); + } +} + +#[tokio::test] +async fn post_model_queries() { + assert_eq!(Post::DB_MODEL_NAME, "posts"); + assert_eq!( + Post::FIND_ALL_QUERY, + "SELECT category_id, order_idx, title, content, author_id FROM posts" + ); + assert_eq!( + Post::FIND_BY_PRIMARY_KEY_QUERY, + "SELECT category_id, order_idx, title, content, author_id FROM posts \ + WHERE category_id = ? AND order_idx = ? AND title = ?" + ); + assert_eq!( + Post::FIND_BY_PARTITION_KEY_QUERY, + "SELECT category_id, order_idx, title, content, author_id FROM posts WHERE category_id = ?" + ); + assert_eq!( + Post::FIND_FIRST_BY_PARTITION_KEY_QUERY, + "SELECT category_id, order_idx, title, content, author_id FROM posts WHERE category_id = ? LIMIT 1" + ); + assert_eq!( + Post::INSERT_QUERY, + "INSERT INTO posts (category_id, order_idx, title, content, author_id) \ + VALUES (:category_id, :order_idx, :title, :content, :author_id)" + ); + assert_eq!( + Post::INSERT_IF_NOT_EXIST_QUERY, + "INSERT INTO posts (category_id, order_idx, title, content, author_id) \ + VALUES (:category_id, :order_idx, :title, :content, :author_id) \ + IF NOT EXISTS" + ); + assert_eq!( + Post::UPDATE_QUERY, + "UPDATE posts SET content = :content, author_id = :author_id \ + WHERE category_id = :category_id AND order_idx = :order_idx AND title = :title" + ); + assert_eq!( + Post::DELETE_QUERY, + "DELETE FROM posts WHERE category_id = ? AND order_idx = ? AND title = ?" + ); + assert_eq!( + Post::DELETE_BY_PARTITION_KEY_QUERY, + "DELETE FROM posts WHERE category_id = ?" + ); +} + +#[tokio::test] +async fn find_various() -> Result<(), CharybdisError> { + let category_id = Uuid::new_v4(); + let db_session = &db_session().await; + + Post::populate_sample_posts_per_partition(category_id).await; + + let posts: CharybdisModelStream = Post::find_by_category_id(category_id).execute(db_session).await?; + assert_eq!(posts.try_collect().await?.len(), 32); + + let posts: CharybdisModelStream = Post::find_by_category_id_and_order_idx(category_id, 1) + .execute(db_session) + .await?; + assert_eq!(posts.try_collect().await?.len(), 1); + + let posts: Post = Post::find_by_category_id_and_order_idx_and_title(category_id, 1, "Post 1".to_string()) + .execute(db_session) + .await?; + assert_eq!(posts.title, "Post 1"); + + let post: Post = Post::find_first_by_category_id(category_id).execute(db_session).await?; + assert_eq!(post.order_idx, 0); + + let post: Post = Post::find_first_by_category_id_and_order_idx(category_id, 1) + .execute(db_session) + .await?; + assert_eq!(post.title, "Post 1"); + + let maybe_post: Option = Post::maybe_find_first_by_category_id(Uuid::new_v4()) + .execute(db_session) + .await?; + + assert!(maybe_post.is_none()); + + let maybe_post: Option = Post::maybe_find_first_by_category_id_and_order_idx(category_id, 1) + .execute(db_session) + .await?; + assert!(maybe_post.is_some()); + + let maybe_post: Option = + Post::maybe_find_first_by_category_id_and_order_idx_and_title(category_id, 2, "Post 2".to_string()) + .execute(db_session) + .await?; + assert!(maybe_post.is_some()); + + // find by local secondary index + let posts: CharybdisModelStream = Post::find_by_category_id_and_title(category_id, "Post 2".to_string()) + .execute(db_session) + .await?; + assert!(posts.try_collect().await?.len() > 0); + + let post: Post = Post::find_first_by_category_id_and_title(category_id, "Post 2".to_string()) + .execute(db_session) + .await?; + assert_eq!(post.title, "Post 2"); + + let maybe_post: Option = Post::maybe_find_first_by_category_id_and_title(category_id, "Post 42".to_string()) + .execute(db_session) + .await?; + assert!(maybe_post.is_none()); + + let author_id = post.author_id; + + // find by global secondary index + let posts: CharybdisModelStream = Post::find_by_author_id(author_id).execute(db_session).await?; + assert!(posts.try_collect().await?.len() > 0); + + let post: Post = Post::find_first_by_author_id(author_id).execute(db_session).await?; + assert_eq!(post.author_id, author_id); + + let post: Option = Post::maybe_find_first_by_author_id(Uuid::new_v4()) + .execute(db_session) + .await?; + assert!(post.is_none()); + + Ok(()) +} diff --git a/charybdis/tests/integrations/query.rs b/charybdis/tests/integrations/query.rs new file mode 100644 index 0000000..67a8d0d --- /dev/null +++ b/charybdis/tests/integrations/query.rs @@ -0,0 +1,156 @@ +use crate::common::db_session; +use crate::model::{Post, User}; +use charybdis::batch::ModelBatch; +use charybdis::errors::CharybdisError; +use charybdis::operations::{Delete, Find, Insert, Update}; +use charybdis::stream::CharybdisModelStream; +use scylla::statement::PagingStateResponse; + +#[tokio::test] +async fn model_mutation() { + let id = uuid::Uuid::new_v4(); + let new_user = User::homer(id); + + let db_session = db_session().await; + + new_user + .insert() + .execute(&db_session) + .await + .expect("Failed to insert user"); + + let mut user = User::find_by_id(id) + .execute(&db_session) + .await + .expect("Failed to find user"); + + assert_eq!(user, new_user); + + user.bio = Some("I like beer".to_string()); + + user.update().execute(&db_session).await.expect("Failed to update user"); + + let user = User::find_by_id(id) + .execute(&db_session) + .await + .expect("Failed to find user"); + + assert_eq!(user.bio, Some("I like beer".to_string())); + + user.delete().execute(&db_session).await.expect("Failed to delete user"); +} + +#[tokio::test] +async fn model_row() { + let id = uuid::Uuid::new_v4(); + let new_user = User::homer(id); + + let db_session = db_session().await; + + new_user + .insert() + .execute(&db_session) + .await + .expect("Failed to insert user"); + + let user = User::find_by_id(id) + .execute(&db_session) + .await + .expect("Failed to find user"); + + assert_eq!(user, new_user); + + user.delete().execute(&db_session).await.expect("Failed to delete user"); +} + +#[tokio::test] +async fn optional_model_row() { + let id = uuid::Uuid::new_v4(); + let db_session = db_session().await; + + let user = User::maybe_find_first_by_id(id) + .execute(&db_session) + .await + .expect("Failed to find user"); + + assert_eq!(user, None); +} + +#[tokio::test] +async fn model_stream() { + let db_session = db_session().await; + + User::populate_sample_users().await; + + let users: CharybdisModelStream = User::find_all() + .execute(&db_session) + .await + .expect("Failed to find users"); + let users_vec = users.try_collect().await.expect("Failed to collect users"); + + assert_eq!(users_vec.len(), 32); + + User::delete_batch() + .chunked_delete(&db_session, &users_vec, 100) + .await + .expect("Failed to delete users"); +} + +#[tokio::test] +async fn model_paged() { + let db_session = db_session().await; + let category_id = uuid::Uuid::new_v4(); + + Post::populate_sample_posts_per_partition(category_id).await; + + let (posts, paging_state_response) = Post::find_by_partition_key_value_paged((category_id,)) + .page_size(3) + .execute(&db_session) + .await + .expect("Failed to find posts"); + + let posts = posts + .collect::, CharybdisError>>() + .expect("Failed to collect posts"); + + assert_eq!(posts.len(), 3); + assert_eq!(posts[0].order_idx, 0); + assert_eq!(posts[1].order_idx, 1); + assert_eq!(posts[2].order_idx, 2); + assert!( + matches!(paging_state_response, PagingStateResponse::HasMorePages { .. }), + "Expected more pages, but got NoMorePages" + ); + + if let PagingStateResponse::HasMorePages { state } = paging_state_response { + let (next_page_posts, paging_state_response) = Post::find_by_partition_key_value_paged((category_id,)) + .page_size(30) // 32 is the total number of posts + .paging_state(state) + .execute(&db_session) + .await + .expect("Failed to find posts"); + let next_page_posts = next_page_posts + .collect::, CharybdisError>>() + .expect("Failed to collect posts"); + + assert_eq!(next_page_posts.len(), 29); + assert_eq!(next_page_posts[0].order_idx, 3); + assert_eq!(next_page_posts[1].order_idx, 4); + assert_eq!(next_page_posts[28].order_idx, 31); + + assert!( + matches!(paging_state_response, PagingStateResponse::NoMorePages), + "Expected no more pages, but got HasMorePages" + ); + + Post::delete_batch() + .chunked_delete(&db_session, &next_page_posts, 100) + .await + .expect("Failed to delete posts"); + } + + Post::delete_batch() + .chunked_delete(&db_session, &posts, 100) + .await + .expect("Failed to delete posts"); +} diff --git a/test/docker-compose.yml b/test/docker-compose.yml new file mode 100644 index 0000000..ca5fe07 --- /dev/null +++ b/test/docker-compose.yml @@ -0,0 +1,80 @@ +version: "3.7" + +networks: + public: + name: charybdis_public + driver: bridge + ipam: + driver: default + config: + - subnet: 172.42.0.0/16 +services: + scylla1: + container_name: scylla1 + image: scylladb/scylla + networks: + public: + ipv4_address: 172.42.0.2 + ports: + - "9042:9042" + command: | + --rpc-address 172.42.0.2 + --listen-address 172.42.0.2 + --seeds 172.42.0.2 + --skip-wait-for-gossip-to-settle 0 + --ring-delay-ms 0 + --smp 2 + --memory 1G + healthcheck: + test: [ "CMD", "cqlsh", "scylla1", "-e", "select * from system.local" ] + interval: 5s + timeout: 5s + retries: 60 + scylla2: + container_name: scylla2 + image: scylladb/scylla + networks: + public: + ipv4_address: 172.42.0.3 + ports: + - "9043:9042" + command: | + --rpc-address 172.42.0.3 + --listen-address 172.42.0.3 + --seeds 172.42.0.2 + --skip-wait-for-gossip-to-settle 0 + --ring-delay-ms 0 + --smp 2 + --memory 1G + healthcheck: + test: [ "CMD", "cqlsh", "scylla2", "-e", "select * from system.local" ] + interval: 5s + timeout: 5s + retries: 60 + depends_on: + scylla1: + condition: service_healthy + scylla3: + container_name: scylla3 + image: scylladb/scylla + networks: + public: + ipv4_address: 172.42.0.4 + ports: + - "9044:9042" + command: | + --rpc-address 172.42.0.4 + --listen-address 172.42.0.4 + --seeds 172.42.0.2,172.42.0.3 + --skip-wait-for-gossip-to-settle 0 + --ring-delay-ms 0 + --smp 2 + --memory 1G + healthcheck: + test: [ "CMD", "cqlsh", "scylla3", "-e", "select * from system.local" ] + interval: 5s + timeout: 5s + retries: 60 + depends_on: + scylla2: + condition: service_healthy