From cdef1a707e6b5746a7a5e37d14e85d2f0b9d694d Mon Sep 17 00:00:00 2001 From: Carter Himmel Date: Thu, 5 Oct 2023 22:32:00 -0600 Subject: [PATCH] refactor: no mames --- Cargo.lock | 9 + Cargo.toml | 27 +- Makefile.toml | 3 +- README.md | 17 +- book/src/README.md | 2 +- book/src/SUMMARY.md | 2 + book/src/delete_queries/README.md | 4 +- book/src/example/README.md | 4 + book/src/query_collections/README.md | 33 + book/src/select_queries/README.md | 2 +- example/Cargo.toml | 28 +- example/expanded.rs | 1178 ++++++++--------- example/src/entities/person/model.rs | 4 +- example/src/entities/person/queries.rs | 54 +- example/src/entities/person_login/model.rs | 4 +- example/src/entities/person_login/queries.rs | 23 +- example/src/main.rs | 16 +- scyllax-cli/Cargo.toml | 20 +- scyllax-cli/src/migrate.rs | 20 +- scyllax-cli/src/model.rs | 16 +- scyllax-macros-core/Cargo.toml | 18 + .../src/entity.rs | 28 +- .../src/enum.rs | 7 +- .../src/json.rs | 7 +- scyllax-macros-core/src/lib.rs | 8 + scyllax-macros-core/src/prepare.rs | 102 ++ scyllax-macros-core/src/queries/mod.rs | 61 + scyllax-macros-core/src/queries/read.rs | 242 ++++ .../src/queries/upsert.rs | 109 +- scyllax-macros-core/src/queries/write.rs | 53 + scyllax-macros/Cargo.toml | 18 +- scyllax-macros/src/lib.rs | 35 +- scyllax-macros/src/queries/delete.rs | 65 - scyllax-macros/src/queries/mod.rs | 3 - scyllax-macros/src/queries/select.rs | 164 --- scyllax-parser/Cargo.toml | 4 +- scyllax-parser/src/select.rs | 38 +- src/collection.rs | 18 + src/entity.rs | 10 + src/error.rs | 4 + src/executor.rs | 97 +- src/lib.rs | 74 +- src/playground.rs | 164 +++ src/prelude.rs | 22 +- src/queries.rs | 30 + src/rows.rs | 2 +- 46 files changed, 1610 insertions(+), 1239 deletions(-) create mode 100644 book/src/example/README.md create mode 100644 book/src/query_collections/README.md create mode 100644 scyllax-macros-core/Cargo.toml rename {scyllax-macros => scyllax-macros-core}/src/entity.rs (80%) rename {scyllax-macros => scyllax-macros-core}/src/enum.rs (95%) rename {scyllax-macros => scyllax-macros-core}/src/json.rs (85%) create mode 100644 scyllax-macros-core/src/lib.rs create mode 100644 scyllax-macros-core/src/prepare.rs create mode 100644 scyllax-macros-core/src/queries/mod.rs create mode 100644 scyllax-macros-core/src/queries/read.rs rename {scyllax-macros => scyllax-macros-core}/src/queries/upsert.rs (67%) create mode 100644 scyllax-macros-core/src/queries/write.rs delete mode 100644 scyllax-macros/src/queries/delete.rs delete mode 100644 scyllax-macros/src/queries/mod.rs delete mode 100644 scyllax-macros/src/queries/select.rs create mode 100644 src/collection.rs create mode 100644 src/entity.rs create mode 100644 src/playground.rs create mode 100644 src/queries.rs diff --git a/Cargo.lock b/Cargo.lock index d97cf78..c417e72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1535,6 +1535,7 @@ dependencies = [ "once_cell", "scylla", "scyllax-macros", + "scyllax-macros-core", "thiserror", "tracing", "uuid", @@ -1568,11 +1569,19 @@ dependencies = [ [[package]] name = "scyllax-macros" version = "0.1.8-alpha" +dependencies = [ + "scyllax-macros-core", +] + +[[package]] +name = "scyllax-macros-core" +version = "0.1.8-alpha" dependencies = [ "anyhow", "darling", "proc-macro2", "quote", + "scyllax-parser", "syn 2.0.29", ] diff --git a/Cargo.toml b/Cargo.toml index fb4d515..581731f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["example", "scyllax-cli", "scyllax-macros", "scyllax-parser"] +members = ["example", "scyllax-cli", "scyllax-macros", "scyllax-macros-core", "scyllax-parser"] resolver = "2" [workspace.package] @@ -19,27 +19,30 @@ readme = "README.md" name = "scyllax" readme = "README.md" description = "A SQLx and Discord inspired query system for Scylla" -version = { workspace = true } -license = { workspace = true } -edition = { workspace = true } -authors = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } +version.workspace = true +license.workspace = true +edition.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true [dependencies] -anyhow = { workspace = true } +anyhow.workspace = true async-trait = "0.1" getrandom = "0.2" mac_address = "1" once_cell = "1" -scylla = { workspace = true } +scylla.workspace = true scyllax-macros = { version = "0.1.8-alpha", path = "./scyllax-macros" } +scyllax-macros-core = { version = "0.1.8-alpha", path = "./scyllax-macros-core" } thiserror = "1" -tracing = { workspace = true } -uuid = { workspace = true } +tracing.workspace = true +uuid.workspace = true [workspace.dependencies] -scyllax-macros = { verison = "0.1.8-alpha", path = "scyllax-macros" } +scyllax-macros = { verison = "0.1.8-alpha", path = "./scyllax-macros" } +scyllax-macros-core = { verison = "0.1.8-alpha", path = "./scyllax-macros-core" } +scyllax-parser = { verison = "0.1.8-alpha", path = "./scyllax-parser" } anyhow = "1" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json", "tracing-log", "parking_lot"] } diff --git a/Makefile.toml b/Makefile.toml index 01533c9..94483a0 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -168,6 +168,5 @@ command = "cargo" args = ["audit"] [tasks.dev-book] -cwd = "book" command = "mdbook" -args = ["serve"] +args = ["serve", "book"] diff --git a/README.md b/README.md index f6fa781..921e105 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,12 @@ pub struct PersonEntity { pub created_at: i64, } ``` -### 2. Select queries -With the [`select_query`] attribute, it's easy to define select queries. +### 2. Read queries +With the [`read_query`] attribute, it's easy to define select queries. ```rust,ignore -#[select_query( - query = "select * from person where id = ? limit 1", - entity_type = "PersonEntity" +#[read_query( + query = "select * from person where id = :id limit 1", + return_type = "PersonEntity" )] pub struct GetPersonById { pub id: Uuid, @@ -42,11 +42,10 @@ pub struct PersonEntity { ``` ## Features -- [x] Select Queries -- [x] Upsert Queries (https://github.com/trufflehq/scyllax/pull/1) -- [x] Delete Queries +- [x] Read Queries +- [x] Write Queries (https://github.com/trufflehq/scyllax/pull/1) - [ ] Request Coalescing -- [ ] Compile-time Select Query Validation +- [x] Compile-time Select Query Validation - ensure the where constraints exist on the struct - ensure the where constraints are the same type as the struct - [ ] Runtime Query Validation (structure matches schema) diff --git a/book/src/README.md b/book/src/README.md index 0aea589..3d0295c 100644 --- a/book/src/README.md +++ b/book/src/README.md @@ -32,6 +32,6 @@ fn main() { let default_keyspace = std::env::var("SCYLLA_DEFAULT_KEYSPACE").ok(); let session = create_session(known_nodes, default_keyspace).await?; - let executor = Executor::with_session(session); + let executor = Executor::::new(session).await?; } ``` diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 0e56855..f8e9d9d 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -9,3 +9,5 @@ - [Select Queries](./select_queries/README.md) - [Delete Queries](./delete_queries/README.md) - [Upsert Queries](./upsert_queries/README.md) +- [Query Collections](./query_collections/README.md) +- [Example](./example/README.md) diff --git a/book/src/delete_queries/README.md b/book/src/delete_queries/README.md index 028a56c..2941f7e 100644 --- a/book/src/delete_queries/README.md +++ b/book/src/delete_queries/README.md @@ -1,7 +1,7 @@ # Delete Queries Writing delete queries, which is pretty much the same as [Select Queries](../select_queries/index.html), is incredibly easy with the `delete_query` macro. -Simply create a struct with the fields you want to select, and annotate it with the `#[delete_query]` macro. +Simply create a struct with the fields you want to select, and annotate it with the `#[write_query]` macro. ```rust #use scyllax::prelude::*; @@ -15,7 +15,7 @@ Simply create a struct with the fields you want to select, and annotate it with # pub created_at: i64, #} # -#[delete_query( +#[write_query( query = "delete from person where id = ?", entity_type = "PersonEntity" )] diff --git a/book/src/example/README.md b/book/src/example/README.md new file mode 100644 index 0000000..1f8c9e3 --- /dev/null +++ b/book/src/example/README.md @@ -0,0 +1,4 @@ +# Example +Are you looking for a real-life example of scyllax? You're in luck! The `exmaple` directory in our GitHub repository contains a full example of scyllax in action -- it's actually used to run tests! + +[https://github.com/trufflehq/scyllax/tree/main/example](https://github.com/trufflehq/scyllax/tree/main/example) diff --git a/book/src/query_collections/README.md b/book/src/query_collections/README.md new file mode 100644 index 0000000..3f7652c --- /dev/null +++ b/book/src/query_collections/README.md @@ -0,0 +1,33 @@ +# Query Collections +Once you've made all your queries, you'll have to make a Query Collection. This is a struct that contains all your prepared queries. Scyllax provides the `create_query_collection` macro to make this easy. + +```rust +use scyllax::prelude::*; +use super::model::UpsertPerson; + +create_query_collection!( + PersonQueries, + [ + GetPersonById, + GetPeopleByIds, + GetPersonByEmail, + DeletePersonById, + UpsertPerson, + ] +); +``` +Then, you use the Query Collection when you instantiate your Executor. + +```rust +let executor = Executor::::new(session).await?; +``` + +Finally, you can run your queries. + +```rust +let person = executor + .execute_read(&GetPersonById { + id: Uuid::new_v4(), + }) + .await?; +``` diff --git a/book/src/select_queries/README.md b/book/src/select_queries/README.md index 63b7458..94e798c 100644 --- a/book/src/select_queries/README.md +++ b/book/src/select_queries/README.md @@ -15,7 +15,7 @@ Simply create a struct with the fields you want to select, and annotate it with # pub created_at: i64, #} # -#[select_query( +#[read_query( query = "select * from person where id = ? limit 1", entity_type = "PersonEntity" )] diff --git a/example/Cargo.toml b/example/Cargo.toml index a919cdb..fcc50a3 100644 --- a/example/Cargo.toml +++ b/example/Cargo.toml @@ -1,24 +1,24 @@ [package] name = "example" description = "an example using scyllax" -version = { workspace = true } -license = { workspace = true } -edition = { workspace = true } -authors = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } +version.workspace = true +license.workspace = true +edition.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true publish = false [dependencies] -anyhow = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -scylla = { workspace = true } +anyhow.workspace = true +serde.workspace = true +serde_json.workspace = true +scylla.workspace = true scyllax = { path = "../" } -tokio = { workspace = true } -tracing = { workspace = true } -tracing-subscriber = { workspace = true } -uuid = { workspace = true } +tokio.workspace = true +tracing.workspace = true +tracing-subscriber.workspace = true +uuid.workspace = true [features] default = ["integration"] diff --git a/example/expanded.rs b/example/expanded.rs index aaab0a2..c2697e3 100644 --- a/example/expanded.rs +++ b/example/expanded.rs @@ -1,466 +1,328 @@ -/// The model itself -pub mod model { +/// All select queries +pub mod queries { use scyllax::prelude::*; - /// Represents data from a person - pub struct PersonData { - /// The stripe id of the person - #[serde(rename = "stripeId")] - pub stripe_id: Option, + use uuid::Uuid; + ///A collection of prepared statements. + #[allow(non_snake_case)] + pub struct PersonQueries { + #[allow(non_snake_case)] + ///The prepared statement for `GetPersonById`. + pub GetPersonById: scylla_reexports::PreparedStatement, + #[allow(non_snake_case)] + ///The prepared statement for `GetPeopleByIds`. + pub GetPeopleByIds: scylla_reexports::PreparedStatement, + #[allow(non_snake_case)] + ///The prepared statement for `GetPersonByEmail`. + pub GetPersonByEmail: scylla_reexports::PreparedStatement, + #[allow(non_snake_case)] + ///The prepared statement for `DeletePersonById`. + pub DeletePersonById: scylla_reexports::PreparedStatement, } - #[automatically_derived] - impl ::core::clone::Clone for PersonData { - #[inline] - fn clone(&self) -> PersonData { - PersonData { - stripe_id: ::core::clone::Clone::clone(&self.stripe_id), - } + ///A collection of prepared statements. + impl scyllax::prelude::QueryCollection for PersonQueries { + #[allow( + clippy::async_yields_async, + clippy::diverging_sub_expression, + clippy::let_unit_value, + clippy::no_effect_underscore_binding, + clippy::shadow_same, + clippy::type_complexity, + clippy::type_repetition_in_bounds, + clippy::used_underscore_binding + )] + fn new<'life0, 'async_trait>( + session: &'life0 scylla::Session, + ) -> ::core::pin::Pin< + Box< + dyn ::core::future::Future< + Output = Result, + > + ::core::marker::Send + 'async_trait, + >, + > + where + 'life0: 'async_trait, + Self: 'async_trait, + { + Box::pin(async move { + if let ::core::option::Option::Some(__ret) = ::core::option::Option::None::< + Result, + > { + return __ret; + } + let __ret: Result = { + Ok(Self { + GetPersonById: session.prepare(GetPersonById::query()).await?, + GetPeopleByIds: session.prepare(GetPeopleByIds::query()).await?, + GetPersonByEmail: session + .prepare(GetPersonByEmail::query()) + .await?, + DeletePersonById: session + .prepare(DeletePersonById::query()) + .await?, + }) + }; + #[allow(unreachable_code)] __ret + }) + } + } + impl scyllax::prelude::GetPreparedStatement for PersonQueries { + ///Get a prepared statement. + fn get(&self) -> &scyllax::prelude::scylla_reexports::PreparedStatement { + &self.GetPersonById + } + } + impl scyllax::prelude::GetPreparedStatement for PersonQueries { + ///Get a prepared statement. + fn get(&self) -> &scyllax::prelude::scylla_reexports::PreparedStatement { + &self.GetPeopleByIds + } + } + impl scyllax::prelude::GetPreparedStatement for PersonQueries { + ///Get a prepared statement. + fn get(&self) -> &scyllax::prelude::scylla_reexports::PreparedStatement { + &self.GetPersonByEmail + } + } + impl scyllax::prelude::GetPreparedStatement for PersonQueries { + ///Get a prepared statement. + fn get(&self) -> &scyllax::prelude::scylla_reexports::PreparedStatement { + &self.DeletePersonById + } + } + /// Get a [`super::model::PersonEntity`] by its [`uuid::Uuid`] + pub struct GetPersonById { + /// The [`uuid::Uuid`] of the [`super::model::PersonEntity`] to get + pub id: Uuid, + } + impl scylla::_macro_internal::ValueList for GetPersonById { + fn serialized(&self) -> scylla::_macro_internal::SerializedResult { + let mut result = scylla::_macro_internal::SerializedValues::with_capacity( + 1usize, + ); + result.add_value(&self.id)?; + ::std::result::Result::Ok(::std::borrow::Cow::Owned(result)) } } #[automatically_derived] - impl ::core::fmt::Debug for PersonData { + impl ::core::fmt::Debug for GetPersonById { fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { ::core::fmt::Formatter::debug_struct_field1_finish( f, - "PersonData", - "stripe_id", - &&self.stripe_id, + "GetPersonById", + "id", + &&self.id, ) } } #[automatically_derived] - impl ::core::marker::StructuralPartialEq for PersonData {} - #[automatically_derived] - impl ::core::cmp::PartialEq for PersonData { + impl ::core::clone::Clone for GetPersonById { #[inline] - fn eq(&self, other: &PersonData) -> bool { - self.stripe_id == other.stripe_id - } - } - #[doc(hidden)] - #[allow(non_upper_case_globals, unused_attributes, unused_qualifications)] - const _: () = { - #[allow(unused_extern_crates, clippy::useless_attribute)] - extern crate serde as _serde; - #[automatically_derived] - impl _serde::Serialize for PersonData { - fn serialize<__S>( - &self, - __serializer: __S, - ) -> _serde::__private::Result<__S::Ok, __S::Error> - where - __S: _serde::Serializer, - { - let mut __serde_state = _serde::Serializer::serialize_struct( - __serializer, - "PersonData", - false as usize + 1, - )?; - _serde::ser::SerializeStruct::serialize_field( - &mut __serde_state, - "stripeId", - &self.stripe_id, - )?; - _serde::ser::SerializeStruct::end(__serde_state) + fn clone(&self) -> GetPersonById { + GetPersonById { + id: ::core::clone::Clone::clone(&self.id), } } - }; - #[doc(hidden)] - #[allow(non_upper_case_globals, unused_attributes, unused_qualifications)] - const _: () = { - #[allow(unused_extern_crates, clippy::useless_attribute)] - extern crate serde as _serde; - #[automatically_derived] - impl<'de> _serde::Deserialize<'de> for PersonData { - fn deserialize<__D>( - __deserializer: __D, - ) -> _serde::__private::Result - where - __D: _serde::Deserializer<'de>, - { - #[allow(non_camel_case_types)] - #[doc(hidden)] - enum __Field { - __field0, - __ignore, - } - #[doc(hidden)] - struct __FieldVisitor; - impl<'de> _serde::de::Visitor<'de> for __FieldVisitor { - type Value = __Field; - fn expecting( - &self, - __formatter: &mut _serde::__private::Formatter, - ) -> _serde::__private::fmt::Result { - _serde::__private::Formatter::write_str( - __formatter, - "field identifier", - ) - } - fn visit_u64<__E>( - self, - __value: u64, - ) -> _serde::__private::Result - where - __E: _serde::de::Error, - { - match __value { - 0u64 => _serde::__private::Ok(__Field::__field0), - _ => _serde::__private::Ok(__Field::__ignore), - } - } - fn visit_str<__E>( - self, - __value: &str, - ) -> _serde::__private::Result - where - __E: _serde::de::Error, - { - match __value { - "stripeId" => _serde::__private::Ok(__Field::__field0), - _ => _serde::__private::Ok(__Field::__ignore), - } - } - fn visit_bytes<__E>( - self, - __value: &[u8], - ) -> _serde::__private::Result - where - __E: _serde::de::Error, - { - match __value { - b"stripeId" => _serde::__private::Ok(__Field::__field0), - _ => _serde::__private::Ok(__Field::__ignore), - } - } - } - impl<'de> _serde::Deserialize<'de> for __Field { - #[inline] - fn deserialize<__D>( - __deserializer: __D, - ) -> _serde::__private::Result - where - __D: _serde::Deserializer<'de>, - { - _serde::Deserializer::deserialize_identifier( - __deserializer, - __FieldVisitor, - ) - } - } - #[doc(hidden)] - struct __Visitor<'de> { - marker: _serde::__private::PhantomData, - lifetime: _serde::__private::PhantomData<&'de ()>, + } + #[automatically_derived] + impl ::core::marker::StructuralPartialEq for GetPersonById {} + #[automatically_derived] + impl ::core::cmp::PartialEq for GetPersonById { + #[inline] + fn eq(&self, other: &GetPersonById) -> bool { + self.id == other.id + } + } + #[automatically_derived] + impl ::core::hash::Hash for GetPersonById { + #[inline] + fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) -> () { + ::core::hash::Hash::hash(&self.id, state) + } + } + impl scyllax::prelude::Query for GetPersonById { + fn query() -> String { + "select * from person where id = :id limit 1".to_string() + } + fn bind(&self) -> scyllax::prelude::SerializedValuesResult { + let mut values = scylla_reexports::value::SerializedValues::new(); + values.add_named_value("id", &self.id)?; + Ok(values) + } + } + impl scyllax::prelude::ReadQuery for GetPersonById { + type Output = Option; + #[allow( + clippy::async_yields_async, + clippy::diverging_sub_expression, + clippy::let_unit_value, + clippy::no_effect_underscore_binding, + clippy::shadow_same, + clippy::type_complexity, + clippy::type_repetition_in_bounds, + clippy::used_underscore_binding + )] + fn parse_response<'async_trait>( + res: scylla::QueryResult, + ) -> ::core::pin::Pin< + Box< + dyn ::core::future::Future< + Output = Result, + > + ::core::marker::Send + 'async_trait, + >, + > + where + Self: 'async_trait, + { + Box::pin(async move { + if let ::core::option::Option::Some(__ret) = ::core::option::Option::None::< + Result, + > { + return __ret; } - impl<'de> _serde::de::Visitor<'de> for __Visitor<'de> { - type Value = PersonData; - fn expecting( - &self, - __formatter: &mut _serde::__private::Formatter, - ) -> _serde::__private::fmt::Result { - _serde::__private::Formatter::write_str( - __formatter, - "struct PersonData", - ) - } - #[inline] - fn visit_seq<__A>( - self, - mut __seq: __A, - ) -> _serde::__private::Result - where - __A: _serde::de::SeqAccess<'de>, - { - let __field0 = match _serde::de::SeqAccess::next_element::< - Option, - >(&mut __seq)? { - _serde::__private::Some(__value) => __value, - _serde::__private::None => { - return _serde::__private::Err( - _serde::de::Error::invalid_length( - 0usize, - &"struct PersonData with 1 element", - ), - ); - } - }; - _serde::__private::Ok(PersonData { stripe_id: __field0 }) - } - #[inline] - fn visit_map<__A>( - self, - mut __map: __A, - ) -> _serde::__private::Result - where - __A: _serde::de::MapAccess<'de>, - { - let mut __field0: _serde::__private::Option> = _serde::__private::None; - while let _serde::__private::Some(__key) - = _serde::de::MapAccess::next_key::<__Field>(&mut __map)? { - match __key { - __Field::__field0 => { - if _serde::__private::Option::is_some(&__field0) { - return _serde::__private::Err( - <__A::Error as _serde::de::Error>::duplicate_field( - "stripeId", - ), - ); - } - __field0 = _serde::__private::Some( - _serde::de::MapAccess::next_value::< - Option, - >(&mut __map)?, - ); - } + let res = res; + let __ret: Result = { + match res.single_row_typed::() { + Ok(data) => Ok(Some(data)), + Err(err) => { + use scylla::transport::query_result::SingleRowTypedError; + match err { + SingleRowTypedError::BadNumberOfRows(_) => Ok(None), _ => { - let _ = _serde::de::MapAccess::next_value::< - _serde::de::IgnoredAny, - >(&mut __map)?; + { + use ::tracing::__macro_support::Callsite as _; + static CALLSITE: ::tracing::callsite::DefaultCallsite = { + static META: ::tracing::Metadata<'static> = { + ::tracing_core::metadata::Metadata::new( + "event example/src/entities/person/queries.rs:12", + "example::entities::person::queries", + ::tracing::Level::ERROR, + Some("example/src/entities/person/queries.rs"), + Some(12u32), + Some("example::entities::person::queries"), + ::tracing_core::field::FieldSet::new( + &["message"], + ::tracing_core::callsite::Identifier(&CALLSITE), + ), + ::tracing::metadata::Kind::EVENT, + ) + }; + ::tracing::callsite::DefaultCallsite::new(&META) + }; + let enabled = ::tracing::Level::ERROR + <= ::tracing::level_filters::STATIC_MAX_LEVEL + && ::tracing::Level::ERROR + <= ::tracing::level_filters::LevelFilter::current() + && { + let interest = CALLSITE.interest(); + !interest.is_never() + && ::tracing::__macro_support::__is_enabled( + CALLSITE.metadata(), + interest, + ) + }; + if enabled { + (|value_set: ::tracing::field::ValueSet| { + let meta = CALLSITE.metadata(); + ::tracing::Event::dispatch(meta, &value_set); + })({ + #[allow(unused_imports)] + use ::tracing::field::{debug, display, Value}; + let mut iter = CALLSITE.metadata().fields().iter(); + CALLSITE + .metadata() + .fields() + .value_set( + &[ + ( + &iter.next().expect("FieldSet corrupted (this is a bug)"), + Some(&format_args!("err: {0:?}", err) as &dyn Value), + ), + ], + ) + }); + } else { + } + }; + Err(scyllax::prelude::ScyllaxError::SingleRowTyped(err)) } } } - let __field0 = match __field0 { - _serde::__private::Some(__field0) => __field0, - _serde::__private::None => { - _serde::__private::de::missing_field("stripeId")? - } - }; - _serde::__private::Ok(PersonData { stripe_id: __field0 }) } - } - #[doc(hidden)] - const FIELDS: &'static [&'static str] = &["stripeId"]; - _serde::Deserializer::deserialize_struct( - __deserializer, - "PersonData", - FIELDS, - __Visitor { - marker: _serde::__private::PhantomData::, - lifetime: _serde::__private::PhantomData, - }, - ) - } + }; + #[allow(unreachable_code)] __ret + }) + } + } + /// Get many [`super::model::PersonEntity`] by many [`uuid::Uuid`] + pub struct GetPeopleByIds { + /// The [`uuid::Uuid`]s of the [`super::model::PersonEntity`]s to get + pub ids: Vec, + /// The maximum number of [`super::model::PersonEntity`]s to get + pub limit: i32, + } + impl scylla::_macro_internal::ValueList for GetPeopleByIds { + fn serialized(&self) -> scylla::_macro_internal::SerializedResult { + let mut result = scylla::_macro_internal::SerializedValues::with_capacity( + 2usize, + ); + result.add_value(&self.ids)?; + result.add_value(&self.limit)?; + ::std::result::Result::Ok(::std::borrow::Cow::Owned(result)) } - }; - impl scylla::frame::value::Value for PersonData { - fn serialize( - &self, - buf: &mut Vec, - ) -> Result<(), scylla::frame::value::ValueTooBig> { - let data = serde_json::to_vec(self).unwrap(); - as scylla::frame::value::Value>::serialize(&data, buf) - } - } - impl scylla::cql_to_rust::FromCqlVal - for PersonData { - fn from_cql( - cql_val: scylla::frame::response::result::CqlValue, - ) -> Result { - let data = >::from_cql(cql_val)?; - serde_json::from_str(&data) - .ok() - .ok_or(scylla::cql_to_rust::FromCqlValError::BadCqlType) - } - } - /// Represents a person in the database - pub struct PersonEntity { - /// The id of the person - #[pk] - pub id: uuid::Uuid, - /// The email address of the person - pub email: String, - /// The age of the person - pub age: Option, - /// Other data from the person - pub data: Option, - /// The date the person was created - #[rename("createdAt")] - pub created_at: i64, - } - ///Upserts a PersonEntity into the `person` table - pub struct UpsertPerson { - ///The id of the PersonEntity - pub id: uuid::Uuid, - ///The email of the PersonEntity - pub email: scyllax::prelude::MaybeUnset, - ///The age of the PersonEntity - pub age: scyllax::prelude::MaybeUnset>, - ///The data of the PersonEntity - pub data: scyllax::prelude::MaybeUnset>, - ///The created_at of the PersonEntity - pub created_at: scyllax::prelude::MaybeUnset, } #[automatically_derived] - impl ::core::fmt::Debug for UpsertPerson { + impl ::core::fmt::Debug for GetPeopleByIds { fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field5_finish( + ::core::fmt::Formatter::debug_struct_field2_finish( f, - "UpsertPerson", - "id", - &self.id, - "email", - &self.email, - "age", - &self.age, - "data", - &self.data, - "created_at", - &&self.created_at, + "GetPeopleByIds", + "ids", + &self.ids, + "limit", + &&self.limit, ) } } #[automatically_derived] - impl ::core::clone::Clone for UpsertPerson { + impl ::core::clone::Clone for GetPeopleByIds { #[inline] - fn clone(&self) -> UpsertPerson { - UpsertPerson { - id: ::core::clone::Clone::clone(&self.id), - email: ::core::clone::Clone::clone(&self.email), - age: ::core::clone::Clone::clone(&self.age), - data: ::core::clone::Clone::clone(&self.data), - created_at: ::core::clone::Clone::clone(&self.created_at), + fn clone(&self) -> GetPeopleByIds { + GetPeopleByIds { + ids: ::core::clone::Clone::clone(&self.ids), + limit: ::core::clone::Clone::clone(&self.limit), } } } - impl scyllax::UpsertQuery for UpsertPerson { - fn query( - &self, - ) -> Result< - (String, scyllax::prelude::SerializedValues), - scyllax::BuildUpsertQueryError, - > { - let query = "update person set email = :email, age = :age, data = :data, \"createdAt\" = :created_at where id = :id;" - .to_string(); - let mut variables = scylla::frame::value::SerializedValues::new(); - match variables.add_named_value("email", &self.email) { - Ok(_) => {} - Err(scylla::frame::value::SerializeValuesError::TooManyValues) => { - return Err(scyllax::BuildUpsertQueryError::TooManyValues { - field: "email".to_string(), - }); - } - Err( - scylla::frame::value::SerializeValuesError::MixingNamedAndNotNamedValues, - ) => { - return Err( - scyllax::BuildUpsertQueryError::MixingNamedAndNotNamedValues, - ); - } - Err(scylla::frame::value::SerializeValuesError::ValueTooBig(_)) => { - return Err(scyllax::BuildUpsertQueryError::ValueTooBig { - field: "email".to_string(), - }); - } - Err(scylla::frame::value::SerializeValuesError::ParseError) => { - return Err(scyllax::BuildUpsertQueryError::ParseError { - field: "email".to_string(), - }); - } - }; - match variables.add_named_value("age", &self.age) { - Ok(_) => {} - Err(scylla::frame::value::SerializeValuesError::TooManyValues) => { - return Err(scyllax::BuildUpsertQueryError::TooManyValues { - field: "age".to_string(), - }); - } - Err( - scylla::frame::value::SerializeValuesError::MixingNamedAndNotNamedValues, - ) => { - return Err( - scyllax::BuildUpsertQueryError::MixingNamedAndNotNamedValues, - ); - } - Err(scylla::frame::value::SerializeValuesError::ValueTooBig(_)) => { - return Err(scyllax::BuildUpsertQueryError::ValueTooBig { - field: "age".to_string(), - }); - } - Err(scylla::frame::value::SerializeValuesError::ParseError) => { - return Err(scyllax::BuildUpsertQueryError::ParseError { - field: "age".to_string(), - }); - } - }; - match variables.add_named_value("data", &self.data) { - Ok(_) => {} - Err(scylla::frame::value::SerializeValuesError::TooManyValues) => { - return Err(scyllax::BuildUpsertQueryError::TooManyValues { - field: "data".to_string(), - }); - } - Err( - scylla::frame::value::SerializeValuesError::MixingNamedAndNotNamedValues, - ) => { - return Err( - scyllax::BuildUpsertQueryError::MixingNamedAndNotNamedValues, - ); - } - Err(scylla::frame::value::SerializeValuesError::ValueTooBig(_)) => { - return Err(scyllax::BuildUpsertQueryError::ValueTooBig { - field: "data".to_string(), - }); - } - Err(scylla::frame::value::SerializeValuesError::ParseError) => { - return Err(scyllax::BuildUpsertQueryError::ParseError { - field: "data".to_string(), - }); - } - }; - match variables.add_named_value("created_at", &self.created_at) { - Ok(_) => {} - Err(scylla::frame::value::SerializeValuesError::TooManyValues) => { - return Err(scyllax::BuildUpsertQueryError::TooManyValues { - field: "created_at".to_string(), - }); - } - Err( - scylla::frame::value::SerializeValuesError::MixingNamedAndNotNamedValues, - ) => { - return Err( - scyllax::BuildUpsertQueryError::MixingNamedAndNotNamedValues, - ); - } - Err(scylla::frame::value::SerializeValuesError::ValueTooBig(_)) => { - return Err(scyllax::BuildUpsertQueryError::ValueTooBig { - field: "created_at".to_string(), - }); - } - Err(scylla::frame::value::SerializeValuesError::ParseError) => { - return Err(scyllax::BuildUpsertQueryError::ParseError { - field: "created_at".to_string(), - }); - } - }; - match variables.add_named_value("id", &self.id) { - Ok(_) => {} - Err(scylla::frame::value::SerializeValuesError::TooManyValues) => { - return Err(scyllax::BuildUpsertQueryError::TooManyValues { - field: "id".to_string(), - }); - } - Err( - scylla::frame::value::SerializeValuesError::MixingNamedAndNotNamedValues, - ) => { - return Err( - scyllax::BuildUpsertQueryError::MixingNamedAndNotNamedValues, - ); - } - Err(scylla::frame::value::SerializeValuesError::ValueTooBig(_)) => { - return Err(scyllax::BuildUpsertQueryError::ValueTooBig { - field: "id".to_string(), - }); - } - Err(scylla::frame::value::SerializeValuesError::ParseError) => { - return Err(scyllax::BuildUpsertQueryError::ParseError { - field: "id".to_string(), - }); - } - }; - Ok((query, variables)) + #[automatically_derived] + impl ::core::marker::StructuralPartialEq for GetPeopleByIds {} + #[automatically_derived] + impl ::core::cmp::PartialEq for GetPeopleByIds { + #[inline] + fn eq(&self, other: &GetPeopleByIds) -> bool { + self.ids == other.ids && self.limit == other.limit } + } + #[automatically_derived] + impl ::core::hash::Hash for GetPeopleByIds { + #[inline] + fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) -> () { + ::core::hash::Hash::hash(&self.ids, state); + ::core::hash::Hash::hash(&self.limit, state) + } + } + impl scyllax::prelude::Query for GetPeopleByIds { + fn query() -> String { + "select * from person where id in ? limit ?".to_string() + } + fn bind(&self) -> scyllax::prelude::SerializedValuesResult { + let mut values = scylla_reexports::value::SerializedValues::new(); + values.add_named_value("ids", &self.ids)?; + values.add_named_value("limit", &self.limit)?; + Ok(values) + } + } + impl scyllax::prelude::ReadQuery for GetPeopleByIds { + type Output = Vec; #[allow( clippy::async_yields_async, clippy::diverging_sub_expression, @@ -471,241 +333,311 @@ pub mod model { clippy::type_repetition_in_bounds, clippy::used_underscore_binding )] - fn execute<'life0, 'async_trait>( - self, - db: &'life0 scyllax::Executor, + fn parse_response<'async_trait>( + res: scylla::QueryResult, ) -> ::core::pin::Pin< Box< dyn ::core::future::Future< - Output = Result, + Output = Result, > + ::core::marker::Send + 'async_trait, >, > where - 'life0: 'async_trait, Self: 'async_trait, { Box::pin(async move { - if let ::core::option::Option::Some(__ret) - = ::core::option::Option::None::< - Result, - > { + if let ::core::option::Option::Some(__ret) = ::core::option::Option::None::< + Result, + > { return __ret; } - let __self = self; - let __ret: Result = { - let (query, values) = Self::query(&__self)?; - { - use ::tracing::__macro_support::Callsite as _; - static CALLSITE: ::tracing::callsite::DefaultCallsite = { - static META: ::tracing::Metadata<'static> = { - ::tracing_core::metadata::Metadata::new( - "event example/src/entities/person/model.rs:13", - "example::entities::person::model", - ::tracing::Level::DEBUG, - Some("example/src/entities/person/model.rs"), - Some(13u32), - Some("example::entities::person::model"), - ::tracing_core::field::FieldSet::new( - &["message", "query", "values"], - ::tracing_core::callsite::Identifier(&CALLSITE), - ), - ::tracing::metadata::Kind::EVENT, - ) - }; - ::tracing::callsite::DefaultCallsite::new(&META) - }; - let enabled = ::tracing::Level::DEBUG - <= ::tracing::level_filters::STATIC_MAX_LEVEL - && ::tracing::Level::DEBUG - <= ::tracing::level_filters::LevelFilter::current() - && { - let interest = CALLSITE.interest(); - !interest.is_never() - && ::tracing::__macro_support::__is_enabled( - CALLSITE.metadata(), - interest, - ) - }; - if enabled { - (|value_set: ::tracing::field::ValueSet| { - let meta = CALLSITE.metadata(); - ::tracing::Event::dispatch(meta, &value_set); - })({ - #[allow(unused_imports)] - use ::tracing::field::{debug, display, Value}; - let mut iter = CALLSITE.metadata().fields().iter(); - CALLSITE - .metadata() - .fields() - .value_set( - &[ - ( - &iter.next().expect("FieldSet corrupted (this is a bug)"), - Some(&format_args!("executing upsert") as &dyn Value), - ), - ( - &iter.next().expect("FieldSet corrupted (this is a bug)"), - Some(&debug(&query) as &dyn Value), - ), - ( - &iter.next().expect("FieldSet corrupted (this is a bug)"), - Some(&values.len() as &dyn Value), + let res = res; + let __ret: Result = { + match res.rows_typed::() { + Ok(xs) => { + Ok( + xs + .filter_map(|x| x.ok()) + .collect::>(), + ) + } + Err(e) => { + { + use ::tracing::__macro_support::Callsite as _; + static CALLSITE: ::tracing::callsite::DefaultCallsite = { + static META: ::tracing::Metadata<'static> = { + ::tracing_core::metadata::Metadata::new( + "event example/src/entities/person/queries.rs:22", + "example::entities::person::queries", + ::tracing::Level::ERROR, + Some("example/src/entities/person/queries.rs"), + Some(22u32), + Some("example::entities::person::queries"), + ::tracing_core::field::FieldSet::new( + &["message"], + ::tracing_core::callsite::Identifier(&CALLSITE), ), - ], - ) - }); - } else { + ::tracing::metadata::Kind::EVENT, + ) + }; + ::tracing::callsite::DefaultCallsite::new(&META) + }; + let enabled = ::tracing::Level::ERROR + <= ::tracing::level_filters::STATIC_MAX_LEVEL + && ::tracing::Level::ERROR + <= ::tracing::level_filters::LevelFilter::current() + && { + let interest = CALLSITE.interest(); + !interest.is_never() + && ::tracing::__macro_support::__is_enabled( + CALLSITE.metadata(), + interest, + ) + }; + if enabled { + (|value_set: ::tracing::field::ValueSet| { + let meta = CALLSITE.metadata(); + ::tracing::Event::dispatch(meta, &value_set); + })({ + #[allow(unused_imports)] + use ::tracing::field::{debug, display, Value}; + let mut iter = CALLSITE.metadata().fields().iter(); + CALLSITE + .metadata() + .fields() + .value_set( + &[ + ( + &iter.next().expect("FieldSet corrupted (this is a bug)"), + Some(&format_args!("err: {0:?}", e) as &dyn Value), + ), + ], + ) + }); + } else { + } + }; + Ok(::alloc::vec::Vec::new()) } - }; - db.session.execute(query, values).await.map_err(|e| e.into()) + } }; #[allow(unreachable_code)] __ret }) } } + /// Get a [`super::model::PersonEntity`] by its email address + pub struct GetPersonByEmail { + /// The email address of the [`super::model::PersonEntity`] to get + pub email: String, + } + impl scylla::_macro_internal::ValueList for GetPersonByEmail { + fn serialized(&self) -> scylla::_macro_internal::SerializedResult { + let mut result = scylla::_macro_internal::SerializedValues::with_capacity( + 1usize, + ); + result.add_value(&self.email)?; + ::std::result::Result::Ok(::std::borrow::Cow::Owned(result)) + } + } #[automatically_derived] - impl ::core::clone::Clone for PersonEntity { + impl ::core::fmt::Debug for GetPersonByEmail { + fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { + ::core::fmt::Formatter::debug_struct_field1_finish( + f, + "GetPersonByEmail", + "email", + &&self.email, + ) + } + } + #[automatically_derived] + impl ::core::clone::Clone for GetPersonByEmail { #[inline] - fn clone(&self) -> PersonEntity { - PersonEntity { - id: ::core::clone::Clone::clone(&self.id), + fn clone(&self) -> GetPersonByEmail { + GetPersonByEmail { email: ::core::clone::Clone::clone(&self.email), - age: ::core::clone::Clone::clone(&self.age), - data: ::core::clone::Clone::clone(&self.data), - created_at: ::core::clone::Clone::clone(&self.created_at), } } } #[automatically_derived] - impl ::core::fmt::Debug for PersonEntity { + impl ::core::marker::StructuralPartialEq for GetPersonByEmail {} + #[automatically_derived] + impl ::core::cmp::PartialEq for GetPersonByEmail { + #[inline] + fn eq(&self, other: &GetPersonByEmail) -> bool { + self.email == other.email + } + } + #[automatically_derived] + impl ::core::hash::Hash for GetPersonByEmail { + #[inline] + fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) -> () { + ::core::hash::Hash::hash(&self.email, state) + } + } + impl scyllax::prelude::Query for GetPersonByEmail { + fn query() -> String { + "select * from person_by_email where email = ? limit 1".to_string() + } + fn bind(&self) -> scyllax::prelude::SerializedValuesResult { + let mut values = scylla_reexports::value::SerializedValues::new(); + values.add_named_value("email", &self.email)?; + Ok(values) + } + } + impl scyllax::prelude::ReadQuery for GetPersonByEmail { + type Output = Option; + #[allow( + clippy::async_yields_async, + clippy::diverging_sub_expression, + clippy::let_unit_value, + clippy::no_effect_underscore_binding, + clippy::shadow_same, + clippy::type_complexity, + clippy::type_repetition_in_bounds, + clippy::used_underscore_binding + )] + fn parse_response<'async_trait>( + res: scylla::QueryResult, + ) -> ::core::pin::Pin< + Box< + dyn ::core::future::Future< + Output = Result, + > + ::core::marker::Send + 'async_trait, + >, + > + where + Self: 'async_trait, + { + Box::pin(async move { + if let ::core::option::Option::Some(__ret) = ::core::option::Option::None::< + Result, + > { + return __ret; + } + let res = res; + let __ret: Result = { + match res.single_row_typed::() { + Ok(data) => Ok(Some(data)), + Err(err) => { + use scylla::transport::query_result::SingleRowTypedError; + match err { + SingleRowTypedError::BadNumberOfRows(_) => Ok(None), + _ => { + { + use ::tracing::__macro_support::Callsite as _; + static CALLSITE: ::tracing::callsite::DefaultCallsite = { + static META: ::tracing::Metadata<'static> = { + ::tracing_core::metadata::Metadata::new( + "event example/src/entities/person/queries.rs:34", + "example::entities::person::queries", + ::tracing::Level::ERROR, + Some("example/src/entities/person/queries.rs"), + Some(34u32), + Some("example::entities::person::queries"), + ::tracing_core::field::FieldSet::new( + &["message"], + ::tracing_core::callsite::Identifier(&CALLSITE), + ), + ::tracing::metadata::Kind::EVENT, + ) + }; + ::tracing::callsite::DefaultCallsite::new(&META) + }; + let enabled = ::tracing::Level::ERROR + <= ::tracing::level_filters::STATIC_MAX_LEVEL + && ::tracing::Level::ERROR + <= ::tracing::level_filters::LevelFilter::current() + && { + let interest = CALLSITE.interest(); + !interest.is_never() + && ::tracing::__macro_support::__is_enabled( + CALLSITE.metadata(), + interest, + ) + }; + if enabled { + (|value_set: ::tracing::field::ValueSet| { + let meta = CALLSITE.metadata(); + ::tracing::Event::dispatch(meta, &value_set); + })({ + #[allow(unused_imports)] + use ::tracing::field::{debug, display, Value}; + let mut iter = CALLSITE.metadata().fields().iter(); + CALLSITE + .metadata() + .fields() + .value_set( + &[ + ( + &iter.next().expect("FieldSet corrupted (this is a bug)"), + Some(&format_args!("err: {0:?}", err) as &dyn Value), + ), + ], + ) + }); + } else { + } + }; + Err(scyllax::prelude::ScyllaxError::SingleRowTyped(err)) + } + } + } + } + }; + #[allow(unreachable_code)] __ret + }) + } + } + /// Get a [`super::model::PersonEntity`] by its [`uuid::Uuid`] + pub struct DeletePersonById { + /// The [`uuid::Uuid`] of the [`super::model::PersonEntity`] to get + pub id: Uuid, + } + #[automatically_derived] + impl ::core::fmt::Debug for DeletePersonById { fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::debug_struct_field5_finish( + ::core::fmt::Formatter::debug_struct_field1_finish( f, - "PersonEntity", + "DeletePersonById", "id", - &self.id, - "email", - &self.email, - "age", - &self.age, - "data", - &self.data, - "created_at", - &&self.created_at, + &&self.id, ) } } #[automatically_derived] - impl ::core::marker::StructuralPartialEq for PersonEntity {} - #[automatically_derived] - impl ::core::cmp::PartialEq for PersonEntity { + impl ::core::clone::Clone for DeletePersonById { #[inline] - fn eq(&self, other: &PersonEntity) -> bool { - self.id == other.id && self.email == other.email && self.age == other.age - && self.data == other.data && self.created_at == other.created_at - } - } - impl scylla::_macro_internal::FromRow for PersonEntity { - fn from_row( - row: scylla::_macro_internal::Row, - ) -> ::std::result::Result { - use scylla::_macro_internal::{CqlValue, FromCqlVal, FromRow, FromRowError}; - use ::std::result::Result::{Ok, Err}; - use ::std::iter::{Iterator, IntoIterator}; - if 5usize != row.columns.len() { - return Err(FromRowError::WrongRowSize { - expected: 5usize, - actual: row.columns.len(), - }); + fn clone(&self) -> DeletePersonById { + DeletePersonById { + id: ::core::clone::Clone::clone(&self.id), } - let mut vals_iter = row.columns.into_iter().enumerate(); - Ok(PersonEntity { - id: { - let (col_ix, col_value) = vals_iter.next().unwrap(); - , - >>::from_cql(col_value) - .map_err(|e| FromRowError::BadCqlVal { - err: e, - column: col_ix, - })? - }, - email: { - let (col_ix, col_value) = vals_iter.next().unwrap(); - , - >>::from_cql(col_value) - .map_err(|e| FromRowError::BadCqlVal { - err: e, - column: col_ix, - })? - }, - age: { - let (col_ix, col_value) = vals_iter.next().unwrap(); - as FromCqlVal< - ::std::option::Option, - >>::from_cql(col_value) - .map_err(|e| FromRowError::BadCqlVal { - err: e, - column: col_ix, - })? - }, - data: { - let (col_ix, col_value) = vals_iter.next().unwrap(); - as FromCqlVal< - ::std::option::Option, - >>::from_cql(col_value) - .map_err(|e| FromRowError::BadCqlVal { - err: e, - column: col_ix, - })? - }, - created_at: { - let (col_ix, col_value) = vals_iter.next().unwrap(); - , - >>::from_cql(col_value) - .map_err(|e| FromRowError::BadCqlVal { - err: e, - column: col_ix, - })? - }, - }) } } - impl scylla::_macro_internal::ValueList for PersonEntity { - fn serialized(&self) -> scylla::_macro_internal::SerializedResult { - let mut result = scylla::_macro_internal::SerializedValues::with_capacity( - 5usize, - ); - result.add_value(&self.id)?; - result.add_value(&self.email)?; - result.add_value(&self.age)?; - result.add_value(&self.data)?; - result.add_value(&self.created_at)?; - ::std::result::Result::Ok(::std::borrow::Cow::Owned(result)) + #[automatically_derived] + impl ::core::marker::StructuralPartialEq for DeletePersonById {} + #[automatically_derived] + impl ::core::cmp::PartialEq for DeletePersonById { + #[inline] + fn eq(&self, other: &DeletePersonById) -> bool { + self.id == other.id } } - impl scyllax::EntityExt for PersonEntity { - fn keys() -> Vec { - <[_]>::into_vec( - #[rustc_box] - ::alloc::boxed::Box::new([ - "id".to_string(), - "email".to_string(), - "age".to_string(), - "data".to_string(), - "\"createdAt\"".to_string(), - ]), - ) + #[automatically_derived] + impl ::core::hash::Hash for DeletePersonById { + #[inline] + fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) -> () { + ::core::hash::Hash::hash(&self.id, state) + } + } + impl scyllax::prelude::Query for DeletePersonById { + fn query() -> String { + "delete from person where id = :id".to_string() } - fn pks() -> Vec { - <[_]>::into_vec(#[rustc_box] ::alloc::boxed::Box::new(["id".to_string()])) + fn bind(&self) -> scyllax::prelude::SerializedValuesResult { + let mut values = scylla_reexports::value::SerializedValues::new(); + values.add_named_value("id", &self.id)?; + Ok(values) } } } diff --git a/example/src/entities/person/model.rs b/example/src/entities/person/model.rs index 7ab403d..e82cdb6 100644 --- a/example/src/entities/person/model.rs +++ b/example/src/entities/person/model.rs @@ -44,6 +44,7 @@ mod test { use super::*; use crate::entities::person::model::UpsertPerson; use pretty_assertions::assert_eq; + use scylla::frame::value::SerializedValues; #[test] fn test_pks() { @@ -78,7 +79,8 @@ mod test { created_at: MaybeUnset::Unset, }; - let (query, values) = upsert.query().expect("failed to parse into query"); + let query = ::query(); + let values = ::bind(&upsert).unwrap(); assert_eq!( query, diff --git a/example/src/entities/person/queries.rs b/example/src/entities/person/queries.rs index e7e6e43..a125479 100644 --- a/example/src/entities/person/queries.rs +++ b/example/src/entities/person/queries.rs @@ -1,21 +1,22 @@ -use scyllax::{delete_query, prelude::*}; +use super::model::UpsertPerson; +use scyllax::prelude::*; use uuid::Uuid; -/// Load all queries for this entity -#[tracing::instrument(skip(db))] -pub async fn load(db: &mut Executor) -> anyhow::Result<()> { - let _ = GetPersonById::prepare(db).await; - let _ = GetPeopleByIds::prepare(db).await; - let _ = GetPersonByEmail::prepare(db).await; - let _ = DeletePersonById::prepare(db).await; - - Ok(()) -} +create_query_collection!( + PersonQueries, + [ + GetPersonById, + GetPeopleByIds, + GetPersonByEmail, + DeletePersonById, + UpsertPerson, + ] +); /// Get a [`super::model::PersonEntity`] by its [`uuid::Uuid`] -#[select_query( - query = "select * from person where id = ? limit 1", - entity_type = "super::model::PersonEntity" +#[read_query( + query = "select * from person where id = :id limit 1", + return_type = "super::model::PersonEntity" )] pub struct GetPersonById { /// The [`uuid::Uuid`] of the [`super::model::PersonEntity`] to get @@ -23,9 +24,9 @@ pub struct GetPersonById { } /// Get many [`super::model::PersonEntity`] by many [`uuid::Uuid`] -#[select_query( - query = "select * from person where id in ? limit ?", - entity_type = "Vec" +#[read_query( + query = "select * from person where id in :ids limit :limit", + return_type = "Vec" )] pub struct GetPeopleByIds { /// The [`uuid::Uuid`]s of the [`super::model::PersonEntity`]s to get @@ -35,9 +36,9 @@ pub struct GetPeopleByIds { } /// Get a [`super::model::PersonEntity`] by its email address -#[select_query( - query = "select * from person_by_email where email = ? limit 1", - entity_type = "super::model::PersonEntity" +#[read_query( + query = "select * from person_by_email where email = :email limit 1", + return_type = "super::model::PersonEntity" )] pub struct GetPersonByEmail { /// The email address of the [`super::model::PersonEntity`] to get @@ -45,10 +46,7 @@ pub struct GetPersonByEmail { } /// Get a [`super::model::PersonEntity`] by its [`uuid::Uuid`] -#[delete_query( - query = "delete from person where id = ?", - entity_type = "super::model::PersonEntity" -)] +#[write_query(query = "delete from person where id = :id")] pub struct DeletePersonById { /// The [`uuid::Uuid`] of the [`super::model::PersonEntity`] to get pub id: Uuid, @@ -65,7 +63,7 @@ mod test { assert_eq!( GetPersonById::query(), - r#"select id, email, age, data, kind, "createdAt" from person where id = ? limit 1"# + r#"select id, email, age, data, kind, "createdAt" from person where id = :id limit 1"# ); } @@ -78,7 +76,7 @@ mod test { assert_eq!( GetPeopleByIds::query(), - r#"select id, email, age, data, kind, "createdAt" from person where id in ? limit ?"# + r#"select id, email, age, data, kind, "createdAt" from person where id in :ids limit :limit"# ); } @@ -90,7 +88,7 @@ mod test { assert_eq!( GetPersonByEmail::query(), - r#"select id, email, age, data, kind, "createdAt" from person_by_email where email = ? limit 1"# + r#"select id, email, age, data, kind, "createdAt" from person_by_email where email = :email limit 1"# ); } @@ -100,7 +98,7 @@ mod test { assert_eq!( DeletePersonById::query(), - r#"delete from person where id = ?"# + r#"delete from person where id = :id"# ); } } diff --git a/example/src/entities/person_login/model.rs b/example/src/entities/person_login/model.rs index bb041a1..989337f 100644 --- a/example/src/entities/person_login/model.rs +++ b/example/src/entities/person_login/model.rs @@ -19,6 +19,7 @@ pub struct PersonLoginEntity { mod test { use super::*; use pretty_assertions::assert_eq; + use scylla::frame::value::SerializedValues; #[test] fn test_pks() { @@ -48,7 +49,8 @@ mod test { count: 1.into(), }; - let (query, values) = upsert.query().expect("failed to parse into query"); + let query = ::query(); + let values = ::bind(&upsert).unwrap(); assert_eq!( query, diff --git a/example/src/entities/person_login/queries.rs b/example/src/entities/person_login/queries.rs index 36603be..43cde8a 100644 --- a/example/src/entities/person_login/queries.rs +++ b/example/src/entities/person_login/queries.rs @@ -1,10 +1,16 @@ -use scyllax::{delete_query, prelude::*}; +use super::model::UpsertPersonLogin; +use scyllax::prelude::*; use uuid::Uuid; +create_query_collection!( + PersonLoginQueries, + [GetPersonLoginById, DeletePersonLoginById, UpsertPersonLogin,] +); + /// Get a [`super::model::PersonLoginEntity`] by its [`uuid::Uuid`] -#[select_query( - query = "select * from person_login where id = ? limit 1", - entity_type = "super::model::PersonLoginEntity" +#[read_query( + query = "select * from person_login where id = :id limit 1", + return_type = "super::model::PersonLoginEntity" )] pub struct GetPersonLoginById { /// The [`uuid::Uuid`] of the [`super::model::PersonEntity`] to get @@ -12,10 +18,7 @@ pub struct GetPersonLoginById { } /// Get a [`super::model::PersonLoginEntity`] by its [`uuid::Uuid`] -#[delete_query( - query = "delete from person_login where id = ?", - entity_type = "super::model::PersonLoginEntity" -)] +#[write_query(query = "delete from person_login where id = :id")] pub struct DeletePersonLoginById { /// The [`uuid::Uuid`] of the [`super::model::PersonLoginEntity`] to get pub id: Uuid, @@ -32,7 +35,7 @@ mod test { assert_eq!( GetPersonLoginById::query(), - r#"select id, person_id, count from person_login where id = ? limit 1"# + r#"select id, person_id, count from person_login where id = :id limit 1"# ); } @@ -42,7 +45,7 @@ mod test { assert_eq!( DeletePersonLoginById::query(), - r#"delete from person_login where id = ?"# + r#"delete from person_login where id = :id"# ); } } diff --git a/example/src/main.rs b/example/src/main.rs index 5b2b71c..718b8a3 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -1,7 +1,7 @@ //! Example use entities::person::{ model::{PersonData, PersonKind, UpsertPerson}, - queries::{load, DeletePersonById, GetPeopleByIds, GetPersonByEmail, GetPersonById}, + queries::{DeletePersonById, GetPeopleByIds, GetPersonByEmail, GetPersonById, PersonQueries}, }; use scyllax::prelude::*; use scyllax::{executor::create_session, util::v1_uuid}; @@ -22,22 +22,20 @@ async fn main() -> anyhow::Result<()> { let default_keyspace = std::env::var("SCYLLA_DEFAULT_KEYSPACE").ok(); let session = create_session(known_nodes, default_keyspace).await?; - let mut executor = Executor::with_session(session); - - load(&mut executor).await?; + let executor = Executor::::new(session).await?; let query = GetPersonByEmail { email: "foo1@scyllax.local".to_string(), }; let res_one = executor - .execute_select(query) + .execute_read(&query) .await? .expect("person not found"); tracing::info!("GetPersonByEmail returned: {:?}", res_one); let query = GetPersonById { id: res_one.id }; let res_two = executor - .execute_select(query) + .execute_read(&query) .await? .expect("person not found"); tracing::info!("GetPersonById returned: {:?}", res_two); @@ -54,7 +52,7 @@ async fn main() -> anyhow::Result<()> { limit: ids.len() as i32, ids, }; - let res = executor.execute_select(query).await?; + let res = executor.execute_read(&query).await?; tracing::info!("GetPeopleByIds returned: {:?}", res); let upsert_id = v1_uuid(); @@ -68,11 +66,11 @@ async fn main() -> anyhow::Result<()> { kind: MaybeUnset::Set(PersonKind::Parent), created_at: MaybeUnset::Unset, }; - let res = executor.execute_upsert(query).await?; + let res = executor.execute_write(&query).await?; tracing::info!("UpsertPerson returned: {:?}", res); let delete = DeletePersonById { id: upsert_id }; - let res = executor.execute_delete(delete).await?; + let res = executor.execute_write(&delete).await?; tracing::info!("DeletePersonById returned: {:?}", res); Ok(()) diff --git a/scyllax-cli/Cargo.toml b/scyllax-cli/Cargo.toml index a5b7233..31365c0 100644 --- a/scyllax-cli/Cargo.toml +++ b/scyllax-cli/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "scyllax-cli" description = "The CLI for scyllax -- mainly managing migrations" -version = { workspace = true } -license = { workspace = true } -edition = { workspace = true } -authors = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } +version.workspace = true +license.workspace = true +edition.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true readme = "../README.md" [[bin]] @@ -14,7 +14,7 @@ name = "scyllax" path = "src/bin/scyllax.rs" [dependencies] -tokio = { workspace = true } +tokio.workspace = true clap = { version = "4", features = ["derive", "env"] } clap_complete = { version = "4" } chrono = { version = "0.4", default-features = false, features = ["clock"] } @@ -23,13 +23,13 @@ url = { version = "2.4.1", default-features = false } async-trait = "0.1" console = "0.15.7" promptly = "0.3.1" -scylla = { workspace = true } +scylla.workspace = true scyllax = { path = "../" } serde_json = "1.0.107" serde = { version = "1.0.188", features = ["derive"] } glob = "0.3.1" -tracing = { workspace = true } +tracing.workspace = true filetime = "0.2.22" time = { version = "0.3.29", features = ["formatting"] } sha2 = "0.10.8" -uuid = { workspace = true } +uuid.workspace = true diff --git a/scyllax-cli/src/migrate.rs b/scyllax-cli/src/migrate.rs index 4bab250..39905af 100644 --- a/scyllax-cli/src/migrate.rs +++ b/scyllax-cli/src/migrate.rs @@ -1,14 +1,14 @@ use anyhow::Context; use console::style; use scylla::{frame::value::Timestamp, query::Query, statement::SerialConsistency}; -use scyllax::{executor::create_session, Executor}; +use scyllax::executor::{create_session, Executor}; use sha2::{Digest, Sha384}; use std::fs::{self, File}; use time::format_description; use crate::{ migrator::MigrationFolder, - model::{DeleteByVersion, GetLatestVersion, UpsertMigration}, + model::{DeleteByVersion, GetLatestVersion, MigrationQueries, UpsertMigration}, opt::ConnectOpts, }; @@ -81,10 +81,10 @@ pub async fn run( // if target_version is specified, then we need to check if we are already at the target version let session = create_session([connect_opts.database_url], Some(connect_opts.keyspace)).await?; - let executor = Executor::with_session(session); + let executor = Executor::::new(session).await?; let current_version = executor - .execute_select(GetLatestVersion {}) + .execute_read(&GetLatestVersion {}) .await? .map_or(-1, |v| v.version); @@ -124,7 +124,7 @@ pub async fn run( // we need to split them where there's a while line that's just a linebreak (\n) // and then execute each statement separately let statements = contents.split("\n\n").collect::>(); - let session = executor.session.get_session(); + let session = &executor.session; println!( "Applying migration {}...", @@ -170,7 +170,7 @@ pub async fn run( checksum: checksum.into(), execution_time: 0.into(), }; - executor.execute_upsert(upsert_row).await?; + executor.execute_write(&upsert_row).await?; // if we are running only the next migration, then we need to stop here if only_next { @@ -188,9 +188,9 @@ pub async fn revert(migration_source: &str, connect_opts: ConnectOpts) -> anyhow // if target_version is specified, then we need to check if we are already at the target version let session = create_session([connect_opts.database_url], Some(connect_opts.keyspace)).await?; - let executor = Executor::with_session(session); + let executor = Executor::::new(session).await?; - let current_version = if let Some(v) = executor.execute_select(GetLatestVersion {}).await? { + let current_version = if let Some(v) = executor.execute_read(&GetLatestVersion {}).await? { v.version } else { println!("No migrations to revert"); @@ -227,7 +227,7 @@ pub async fn revert(migration_source: &str, connect_opts: ConnectOpts) -> anyhow let contents = fs::read_to_string(&up_path).context("Unable to read up file")?; let statements = contents.split("\n\n").collect::>(); - let session = executor.session.get_session(); + let session = &executor.session; println!( "Reverting migration {}...", @@ -261,7 +261,7 @@ pub async fn revert(migration_source: &str, connect_opts: ConnectOpts) -> anyhow ); executor - .execute_delete(DeleteByVersion { + .execute_write(&DeleteByVersion { version: migration.version, }) .await?; diff --git a/scyllax-cli/src/model.rs b/scyllax-cli/src/model.rs index 2c83e07..716b040 100644 --- a/scyllax-cli/src/model.rs +++ b/scyllax-cli/src/model.rs @@ -1,6 +1,11 @@ use scylla::frame::value::Timestamp; use scyllax::prelude::*; +create_query_collection!( + MigrationQueries, + [GetLatestVersion, DeleteByVersion, UpsertMigration,] +); + #[upsert_query(table = "migration", name = UpsertMigration)] #[derive(scylla::FromRow, scylla::ValueList, Debug, Clone, PartialEq, Entity)] pub struct MigrationEntity { @@ -16,17 +21,14 @@ pub struct MigrationEntity { } // get the latest version from the database -#[select_query( - query = "select * from migration where bucket = 0 order by version desc limit 1", - entity_type = "MigrationEntity" +#[read_query( + query_nocheck = "select * from migration where bucket = 0 order by version desc limit 1", + return_type = "MigrationEntity" )] pub struct GetLatestVersion {} /// Delete a migration by its version -#[delete_query( - query = "delete from migration where bucket = 0 and version = ?", - entity_type = "MigrationEntity" -)] +#[write_query(query = "delete from migration where bucket = 0 and version = ?")] pub struct DeleteByVersion { pub version: i64, } diff --git a/scyllax-macros-core/Cargo.toml b/scyllax-macros-core/Cargo.toml new file mode 100644 index 0000000..d07ea3c --- /dev/null +++ b/scyllax-macros-core/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "scyllax-macros-core" +description = "Core macro impl for scyllax" +version.workspace = true +license.workspace = true +edition.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +readme = "../README.md" + +[dependencies] +anyhow.workspace = true +darling = { version = "0.20", features = ["suggestions"] } +proc-macro2 = "1" +quote = "1" +scyllax-parser.workspace = true +syn = { version = "2", features = ["full", "derive", "extra-traits"] } diff --git a/scyllax-macros/src/entity.rs b/scyllax-macros-core/src/entity.rs similarity index 80% rename from scyllax-macros/src/entity.rs rename to scyllax-macros-core/src/entity.rs index 8996331..60fb921 100644 --- a/scyllax-macros/src/entity.rs +++ b/scyllax-macros-core/src/entity.rs @@ -1,14 +1,13 @@ -use crate::token_stream_with_error; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use syn::{Expr, Field, ItemStruct}; /// Attribute expand /// Just adds the dervie macro to the struct. -pub(crate) fn expand(input: TokenStream) -> TokenStream { +pub fn expand(input: TokenStream) -> TokenStream { let input: ItemStruct = match syn::parse2(input.clone()) { Ok(it) => it, - Err(e) => return token_stream_with_error(input, e), + Err(e) => return e.to_compile_error(), }; let input_clone = input.clone(); @@ -19,13 +18,11 @@ pub(crate) fn expand(input: TokenStream) -> TokenStream { .collect::>(); if pks.is_empty() { - return token_stream_with_error( + return syn::Error::new_spanned( input.clone().into_token_stream(), - syn::Error::new_spanned( - input.clone().into_token_stream(), - "Entity can only be derived for structs with at least one #[pk] field.", - ), - ); + "Entity can only be derived for structs with at least one #[pk] field.", + ) + .to_compile_error(); } entity_impl(&input, &pks) @@ -39,7 +36,7 @@ fn entity_impl(input: &ItemStruct, pks: &[&Field]) -> TokenStream { let pks = pks.iter().map(|x| get_field_name(x)).collect::>(); quote! { - impl scyllax::EntityExt<#name> for #name { + impl scyllax::prelude::EntityExt<#name> for #name { fn keys() -> Vec { vec![#(#keys.to_string()),*] } @@ -73,9 +70,16 @@ pub(crate) fn get_field_name(field: &Field) -> String { .to_string() } -pub(crate) fn expand_attr(_args: TokenStream, input: TokenStream) -> TokenStream { +pub fn expand_attr(_args: TokenStream, input: TokenStream) -> TokenStream { quote! { - #[derive(Clone, Debug, PartialEq, scyllax::FromRow, scyllax::prelude::ValueList, scyllax::Entity)] + #[derive( + Clone, + Debug, + PartialEq, + scylla_reexports::FromRow, + scylla_reexports::ValueList, + scyllax::prelude::Entity + )] #input } } diff --git a/scyllax-macros/src/enum.rs b/scyllax-macros-core/src/enum.rs similarity index 95% rename from scyllax-macros/src/enum.rs rename to scyllax-macros-core/src/enum.rs index 1b4fe63..8bd0d4d 100644 --- a/scyllax-macros/src/enum.rs +++ b/scyllax-macros-core/src/enum.rs @@ -1,9 +1,8 @@ -use crate::token_stream_with_error; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use syn::ItemEnum; -pub(crate) fn expand_attr(_args: TokenStream, input: TokenStream) -> TokenStream { +pub fn expand_attr(_args: TokenStream, input: TokenStream) -> TokenStream { quote! { #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, scyllax::prelude::IntEnum)] #input @@ -59,10 +58,10 @@ pub(crate) fn expand_attr(_args: TokenStream, input: TokenStream) -> TokenStream // } // } -pub(crate) fn expand(input: TokenStream) -> TokenStream { +pub fn expand(input: TokenStream) -> TokenStream { let input: ItemEnum = match syn::parse2(input.clone()) { Ok(it) => it, - Err(e) => return token_stream_with_error(input, e), + Err(e) => return e.to_compile_error(), }; let ident = &input.ident; diff --git a/scyllax-macros/src/json.rs b/scyllax-macros-core/src/json.rs similarity index 85% rename from scyllax-macros/src/json.rs rename to scyllax-macros-core/src/json.rs index c574c88..5235631 100644 --- a/scyllax-macros/src/json.rs +++ b/scyllax-macros-core/src/json.rs @@ -1,19 +1,18 @@ -use crate::token_stream_with_error; use proc_macro2::TokenStream; use quote::quote; use syn::ItemStruct; -pub(crate) fn expand_attr(_args: TokenStream, input: TokenStream) -> TokenStream { +pub fn expand_attr(_args: TokenStream, input: TokenStream) -> TokenStream { quote! { #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, scyllax::prelude::JsonData)] #input } } -pub(crate) fn expand(input: TokenStream) -> TokenStream { +pub fn expand(input: TokenStream) -> TokenStream { let input: ItemStruct = match syn::parse2(input.clone()) { Ok(it) => it, - Err(e) => return token_stream_with_error(input, e), + Err(e) => return e.to_compile_error(), }; let ident = &input.ident; diff --git a/scyllax-macros-core/src/lib.rs b/scyllax-macros-core/src/lib.rs new file mode 100644 index 0000000..1b7d0c5 --- /dev/null +++ b/scyllax-macros-core/src/lib.rs @@ -0,0 +1,8 @@ +//! Macros supporting scyllax. +//! +//! See the [scyllax docs](https://docs.rs/scyllax) for more information. +pub mod entity; +pub mod r#enum; +pub mod json; +pub mod prepare; +pub mod queries; diff --git a/scyllax-macros-core/src/prepare.rs b/scyllax-macros-core/src/prepare.rs new file mode 100644 index 0000000..d4f0597 --- /dev/null +++ b/scyllax-macros-core/src/prepare.rs @@ -0,0 +1,102 @@ +//! This module contains the `prepare_queries!` macro. +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + ExprArray, +}; + +// // prepare_queries!(PersonQueries, [GetPersonById, GetPeopleByIds, DeletePersonById, ...]); +/// Options for the `prepare_queries!` macro. +pub struct PrepareQueriesInput { + /// The name of the struct to generate. + pub name: syn::Ident, + /// The queries to attach to the struct. + pub queries: Vec, +} + +impl Parse for PrepareQueriesInput { + fn parse(input: ParseStream<'_>) -> syn::Result { + let name = input.parse()?; + input.parse::()?; + + let queries = input + .parse::()? + .elems + .iter() + .map(|expr| { + if let syn::Expr::Path(path) = expr { + Ok(path.path.get_ident().unwrap().clone()) + } else { + Err(syn::Error::new_spanned(expr, "expected an identifier")) + } + }) + .collect::>>()?; + + Ok(Self { name, queries }) + } +} + +// prepare_queries!(PersonQueries, [GetPersonById, GetPeopleByIds, DeletePersonById, ...]); +// creates a struct like this: +// pub struct PersonQueries { +// GetPersonById: scylla::statement::prepared_statement::PreparedStatement, +// GetPeopleByIds: scylla::statement::prepared_statement::PreparedStatement, +// DeletePersonById: scylla::statement::prepared_statement::PreparedStatement, +// ... +// } +/// Expands the `prepare_queries!` macro. +pub fn expand(input: TokenStream) -> TokenStream { + let args: PrepareQueriesInput = match syn::parse2(input) { + Ok(args) => args, + Err(e) => return e.to_compile_error(), + }; + let queries = args.queries; + let name = args.name; + + let stmts = queries.iter().map(|field| { + let doc = format!("The prepared statement for `{}`.", field); + quote! { + #[allow(non_snake_case)] + #[doc = #doc] + pub #field: scylla_reexports::PreparedStatement, + } + }); + + let gets = queries.iter().map(|field| { + quote! { + impl scyllax::prelude::GetPreparedStatement<#field> for #name { + #[doc = "Get a prepared statement."] + fn get(&self) -> &scyllax::prelude::scylla_reexports::PreparedStatement { + &self.#field + } + } + } + }); + + let prepares = queries.iter().map(|field| { + quote! { + #field: session.prepare(#field::query()).await?, + } + }); + + quote! { + #[doc = "A collection of prepared statements."] + #[allow(non_snake_case)] + pub struct #name { + #(#stmts)* + } + + #[scyllax::prelude::async_trait] + #[doc = "A collection of prepared statements."] + impl scyllax::prelude::QueryCollection for #name { + async fn new(session: &scylla::Session) -> Result { + Ok(Self { + #(#prepares)* + }) + } + } + + #(#gets)* + } +} diff --git a/scyllax-macros-core/src/queries/mod.rs b/scyllax-macros-core/src/queries/mod.rs new file mode 100644 index 0000000..8126871 --- /dev/null +++ b/scyllax-macros-core/src/queries/mod.rs @@ -0,0 +1,61 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::ItemStruct; + +pub mod read; +pub mod upsert; +pub mod write; + +/// Creates the [`Query#bind`] function for any query. +/// +/// This will take the query struct create a SerializedValue from each field. +pub fn create_bind_function(item: &ItemStruct) -> TokenStream { + let sets = item + .fields + .iter() + .map(|field| { + let field_name = field.ident.clone().unwrap(); + + quote! { + values.add_named_value(stringify!(#field_name), &self.#field_name)?; + } + }) + .collect::>(); + + quote! { + fn bind(&self) -> scyllax::prelude::SerializedValuesResult { + let mut values = scylla_reexports::value::SerializedValues::new(); + + #(#sets)* + + Ok(values) + } + } +} + +/// Implements the [`Query`] trait for a struct. +pub fn impl_generic_query( + input: &ItemStruct, + query: String, + inner_entity_type: Option<&syn::Type>, +) -> TokenStream { + let struct_ident = &input.ident; + let bind_fn = create_bind_function(input); + + let query = if let Some(inner_entity_type) = inner_entity_type { + quote!(#query.replace("*", &#inner_entity_type::keys().join(", "))) + } else { + quote!(#query.to_string()) + }; + + quote! { + #[scyllax::prelude::async_trait] + impl scyllax::prelude::Query for #struct_ident { + fn query() -> String { + #query + } + + #bind_fn + } + } +} diff --git a/scyllax-macros-core/src/queries/read.rs b/scyllax-macros-core/src/queries/read.rs new file mode 100644 index 0000000..714a75c --- /dev/null +++ b/scyllax-macros-core/src/queries/read.rs @@ -0,0 +1,242 @@ +use darling::{export::NestedMeta, FromMeta}; +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use scyllax_parser::{select::parse_select, SelectQuery, Value, Variable}; +use syn::ItemStruct; + +use crate::queries::impl_generic_query; + +#[derive(FromMeta)] +pub(crate) struct SelectQueryOptions { + query: Option, + query_nocheck: Option, + return_type: syn::Type, +} + +pub fn expand(args: TokenStream, item: TokenStream) -> TokenStream { + let attr_args = match NestedMeta::parse_meta_list(args.clone()) { + Ok(args) => args, + Err(e) => return darling::Error::from(e).write_errors(), + }; + + let args = match SelectQueryOptions::from_list(&attr_args) { + Ok(o) => o, + Err(e) => return e.write_errors(), + }; + + if args.query.is_none() && args.query_nocheck.is_none() { + return syn::Error::new_spanned(item, "Either query or query_nocheck must be specified") + .to_compile_error(); + } + + let return_type = args.return_type; + + let input: ItemStruct = match syn::parse2(item.clone()) { + Ok(it) => it, + Err(e) => return e.to_compile_error(), + }; + let struct_ident = &input.ident; + + // trimmed return_type + // eg: Vec -> OrgEntity + // eg: OrgEntity -> OrgEntity + let inner_entity_type = if let syn::Type::Path(path) = return_type.clone() { + let last_segment = path.path.segments.last().unwrap(); + let ident = &last_segment.ident; + + if ident == "Vec" { + let args = &last_segment.arguments; + if let syn::PathArguments::AngleBracketed(args) = args { + let args = &args.args; + if args.len() != 1 { + return syn::Error::new_spanned( + return_type, + "return_type must be a path with one generic argument", + ) + .to_compile_error(); + } + + if let syn::GenericArgument::Type(ty) = args.first().unwrap() { + ty.clone() + } else { + return syn::Error::new_spanned( + return_type, + "return_type must be a path with one generic argument", + ) + .to_compile_error(); + } + } else { + return syn::Error::new_spanned( + return_type, + "return_type must be a path with one generic argument", + ) + .to_compile_error(); + } + } else { + return_type.clone() + } + } else { + return syn::Error::new_spanned(return_type, "return_type must be a path") + .to_compile_error(); + }; + + // query parsing + let query = if let Some(query) = args.query { + match parse_query(&input, &query) { + Ok(_) => (), + Err(e) => return e.to_compile_error(), + }; + + query + } else if let Some(query) = args.query_nocheck { + query + } else { + unreachable!() + }; + + // if return_type is a Vec, return type is Vec + // if return_type is not a Vec, return type is Option + let impl_return_type = if let syn::Type::Path(path) = return_type.clone() { + let last_segment = path.path.segments.last().unwrap(); + let ident = &last_segment.ident; + + if ident == "Vec" { + quote! { + #return_type + } + } else { + quote! { + Option<#return_type> + } + } + } else { + return syn::Error::new_spanned(return_type, "return_type must be a path") + .to_compile_error(); + }; + + // if return_type is a Vec, we need to use the macro scyllax:match_rows!(res, return_type) + // if return_type is not a Vec, we need to use the macro scyllax:match_row!(res, return_type) + // eg: Vec -> scyllax:match_rows!(res, OrgEntity) + // eg: OrgEntity -> scyllax:match_row!(res, OrgEntity) + let parser = if let syn::Type::Path(path) = return_type.clone() { + let last_segment = path.path.segments.last().unwrap(); + let ident = &last_segment.ident; + + if ident == "Vec" { + quote! { + scyllax::match_rows!(res, #inner_entity_type) + } + } else { + quote! { + scyllax::match_row!(res, #path) + } + } + } else { + return syn::Error::new_spanned(return_type, "return_type must be a path") + .to_compile_error(); + }; + + let impl_query = impl_generic_query(&input, query, Some(&inner_entity_type)); + + quote! { + #[derive(scylla::ValueList, std::fmt::Debug, std::clone::Clone, PartialEq, Hash)] + #input + + #impl_query + + #[scyllax::prelude::async_trait] + impl scyllax::prelude::ReadQuery for #struct_ident { + type Output = #impl_return_type; + + async fn parse_response(res: scylla::QueryResult) -> + Result + { + #parser + } + } + } +} + +fn parse_query(input: &ItemStruct, query: &String) -> Result { + let (rest, parsed) = match parse_select(query) { + Ok(parsed) => parsed, + Err(e) => { + return Err(syn::Error::new_spanned( + query.into_token_stream(), + format!("Failed to parse query: {:#?}", e), + )) + } + }; + + if !rest.is_empty() { + return Err(syn::Error::new_spanned( + query.into_token_stream(), + format!("Failed to parse query, stopped at: {:#?}.\nThe parser's still in development... If you're positive it's valid, rename `query` to `query_nockeck`.", rest), + )); + } + + // only allow named variables in parsed.conditions. no placeholders. + if parsed + .condition + .iter() + .any(|condition| matches!(condition.value, Value::Variable(Variable::Placeholder))) + { + return Err(syn::Error::new_spanned( + query.into_token_stream(), + "Cannot use placeholder variables in query", + )); + } + + // only allow named OR placeholder variables in parsed.conditions, not both. + let (has_named, has_placeholder) = + parsed + .condition + .iter() + .fold( + (false, false), + |(named, placeholder), condition| match condition.value { + Value::Variable(Variable::NamedVariable(_)) => (true, placeholder), + Value::Variable(Variable::Placeholder) => (named, true), + _ => (named, placeholder), + }, + ); + + if has_named && has_placeholder { + return Err(syn::Error::new_spanned( + query.into_token_stream(), + "Cannot mix named and placeholder variables in query", + )); + } + + // check that all variables in parsed.conditions match a field in the struct + let misses = parsed + .condition + .iter() + .filter_map(|condition| match condition.value { + Value::Variable(Variable::NamedVariable(ref name)) => { + if !input + .fields + .iter() + .any(|f| f.ident.as_ref().unwrap() == name) + { + Some(name.clone()) + } else { + None + } + } + _ => None, + }) + .collect::>(); + + if !misses.is_empty() { + return Err(syn::Error::new_spanned( + query.into_token_stream(), + format!( + "Query contains variables that do not match any fields in the struct: {}", + misses.join(", ") + ), + )); + } + + Ok(parsed) +} diff --git a/scyllax-macros/src/queries/upsert.rs b/scyllax-macros-core/src/queries/upsert.rs similarity index 67% rename from scyllax-macros/src/queries/upsert.rs rename to scyllax-macros-core/src/queries/upsert.rs index cd860d0..a2e5169 100644 --- a/scyllax-macros/src/queries/upsert.rs +++ b/scyllax-macros-core/src/queries/upsert.rs @@ -1,10 +1,9 @@ +use crate::entity::get_field_name; use darling::{ast::NestedMeta, FromMeta}; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use syn::{Field, ItemStruct}; -use crate::{entity::get_field_name, token_stream_with_error}; - #[derive(FromMeta)] pub(crate) struct UpsertQueryOptions { pub name: syn::Ident, @@ -13,7 +12,7 @@ pub(crate) struct UpsertQueryOptions { /// Attribute expand /// Just adds the dervie macro to the struct. -pub(crate) fn expand(args: TokenStream, input: TokenStream) -> TokenStream { +pub fn expand(args: TokenStream, input: TokenStream) -> TokenStream { let attr_args = match NestedMeta::parse_meta_list(args.clone()) { Ok(args) => args, Err(e) => return darling::Error::from(e).write_errors(), @@ -26,7 +25,7 @@ pub(crate) fn expand(args: TokenStream, input: TokenStream) -> TokenStream { let input: ItemStruct = match syn::parse2(input.clone()) { Ok(it) => it, - Err(e) => return token_stream_with_error(input, e), + Err(e) => return e.to_compile_error(), }; let input_clone = input.clone(); @@ -46,34 +45,28 @@ pub(crate) fn expand(args: TokenStream, input: TokenStream) -> TokenStream { if let syn::Type::Path(path) = &counter.ty { if let Some(ident) = path.path.get_ident() { if ident != "scylla::frame::value::Counter" { - return token_stream_with_error( - input.into_token_stream(), - syn::Error::new_spanned( - counter.into_token_stream(), - "Counter fields must be of type `scylla::frame::value::Counter`", - ), - ); + return syn::Error::new_spanned( + counter.into_token_stream(), + "Counter fields must be of type `scylla::frame::value::Counter`", + ) + .to_compile_error(); } } } else { - return token_stream_with_error( - input.into_token_stream(), - syn::Error::new_spanned( - counter.into_token_stream(), - "Counter fields must be of type `scylla::frame::value::Counter", - ), - ); + return syn::Error::new_spanned( + counter.into_token_stream(), + "Counter fields must be of type `scylla::frame::value::Counter", + ) + .to_compile_error(); } } if pks.is_empty() { - return token_stream_with_error( + return syn::Error::new_spanned( input.clone().into_token_stream(), - syn::Error::new_spanned( - input.clone().into_token_stream(), - "Entity can only be derived for structs with at least one #[pk] field.", - ), - ); + "Entity can only be derived for structs with at least one #[pk] field.", + ) + .to_compile_error(); } upsert_impl(&input, &args, &pks, &counters) @@ -147,7 +140,6 @@ pub(crate) fn upsert_impl( .map(|f| { let ident = &f.ident.clone().unwrap(); let col = get_field_name(f); - let errors = error_switchback(f); let ident_string = ident.to_string(); let query = if counters.contains(f) { @@ -159,10 +151,7 @@ pub(crate) fn upsert_impl( ( query, quote! { - match variables.add_named_value(#ident_string, &self.#ident) { - Ok(_) => (), - #errors - }; + values.add_named_value(#ident_string, &self.#ident)?; }, ) }) @@ -176,16 +165,12 @@ pub(crate) fn upsert_impl( .map(|f| { let ident = &f.ident.clone().unwrap(); let col = get_field_name(f); - let errors = error_switchback(f); let named_var = ident.to_string(); ( (col.clone(), named_var.clone()), quote! { - match variables.add_named_value(#named_var, &self.#ident) { - Ok(_) => (), - #errors - }; + values.add_named_value(#named_var, &self.#ident)?; }, ) }) @@ -200,32 +185,23 @@ pub(crate) fn upsert_impl( #expanded_upsert_struct - #[scyllax::async_trait] - impl scyllax::UpsertQuery<#struct_ident> for #upsert_struct { - fn query( - &self, - ) -> Result<(String, scyllax::prelude::SerializedValues), scyllax::BuildUpsertQueryError> { - let query = #query.to_string(); - let mut variables = scylla::frame::value::SerializedValues::new(); - - #(#set_sv_push)* - #(#where_sv_push)* - - Ok((query, variables)) + #[scyllax::prelude::async_trait] + impl scyllax::prelude::Query for #upsert_struct { + fn query() -> String { + #query.to_string() } + fn bind(&self) -> scyllax::prelude::SerializedValuesResult { + let mut values = scylla_reexports::value::SerializedValues::new(); - async fn execute(self, db: &scyllax::Executor) -> Result { - let (query, values) = Self::query(&self)?; + #(#set_sv_push)* + #(#where_sv_push)* - tracing::debug! { - query = ?query, - values = values.len(), - "executing upsert" - }; - db.session.execute(query, values).await.map_err(|e| e.into()) + Ok(values) } } + + impl scyllax::prelude::WriteQuery for #upsert_struct {} } } @@ -266,31 +242,6 @@ fn build_query( } } -fn error_switchback(f: &&syn::Field) -> TokenStream { - let ident = &f.ident; - - quote! { - Err(scylla::frame::value::SerializeValuesError::TooManyValues) => { - return Err(scyllax::BuildUpsertQueryError::TooManyValues { - field: stringify!(#ident).to_string(), - }) - } - Err(scylla::frame::value::SerializeValuesError::MixingNamedAndNotNamedValues) => { - return Err(scyllax::BuildUpsertQueryError::MixingNamedAndNotNamedValues) - } - Err(scylla::frame::value::SerializeValuesError::ValueTooBig(_)) => { - return Err(scyllax::BuildUpsertQueryError::ValueTooBig { - field: stringify!(#ident).to_string(), - }) - } - Err(scylla::frame::value::SerializeValuesError::ParseError) => { - return Err(scyllax::BuildUpsertQueryError::ParseError { - field: stringify!(#ident).to_string(), - }) - } - } -} - #[cfg(test)] mod tests { use super::build_query; diff --git a/scyllax-macros-core/src/queries/write.rs b/scyllax-macros-core/src/queries/write.rs new file mode 100644 index 0000000..f071ecb --- /dev/null +++ b/scyllax-macros-core/src/queries/write.rs @@ -0,0 +1,53 @@ +use crate::queries::impl_generic_query; +use darling::{export::NestedMeta, FromMeta}; +use proc_macro2::TokenStream; +use quote::quote; +use syn::ItemStruct; + +#[derive(FromMeta)] +pub(crate) struct WriteQueryOptions { + query: Option, + query_nocheck: Option, +} + +pub fn expand(args: TokenStream, item: TokenStream) -> TokenStream { + let attr_args = match NestedMeta::parse_meta_list(args.clone()) { + Ok(args) => args, + Err(e) => return darling::Error::from(e).write_errors(), + }; + + let args = match WriteQueryOptions::from_list(&attr_args) { + Ok(o) => o, + Err(e) => return e.write_errors(), + }; + + let input: ItemStruct = match syn::parse2(item.clone()) { + Ok(it) => it, + Err(e) => return e.to_compile_error(), + }; + + let query = if let Some(query) = args.query { + // match parse_query(&input, &query) { + // Ok(_) => (), + // Err(e) => return e.to_compile_error(), + // }; + + query + } else if let Some(query) = args.query_nocheck { + query + } else { + unreachable!() + }; + + let impl_query = impl_generic_query(&input, query, None); + let struct_ident = &input.ident; + + quote! { + #[derive(std::fmt::Debug, std::clone::Clone, PartialEq, Hash)] + #input + + #impl_query + + impl scyllax::prelude::WriteQuery for #struct_ident {} + } +} diff --git a/scyllax-macros/Cargo.toml b/scyllax-macros/Cargo.toml index 3625571..8857c4f 100644 --- a/scyllax-macros/Cargo.toml +++ b/scyllax-macros/Cargo.toml @@ -1,20 +1,16 @@ [package] name = "scyllax-macros" description = "Macros for scyllax" -version = { workspace = true } -license = { workspace = true } -edition = { workspace = true } -authors = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } +version.workspace = true +license.workspace = true +edition.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true readme = "../README.md" [lib] proc-macro = true [dependencies] -anyhow = { workspace = true } -darling = { version = "0.20", features = ["suggestions"] } -proc-macro2 = "1" -quote = "1" -syn = { version = "2", features = ["full", "derive", "extra-traits"] } +scyllax-macros-core.workspace = true diff --git a/scyllax-macros/src/lib.rs b/scyllax-macros/src/lib.rs index 4eefd66..485829e 100644 --- a/scyllax-macros/src/lib.rs +++ b/scyllax-macros/src/lib.rs @@ -2,22 +2,12 @@ //! //! See the [scyllax docs](https://docs.rs/scyllax) for more information. use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; - -mod entity; -mod r#enum; -mod json; -mod queries; - -pub(crate) fn token_stream_with_error(mut tokens: TokenStream2, error: syn::Error) -> TokenStream2 { - tokens.extend(error.into_compile_error()); - tokens -} +use scyllax_macros_core::{entity, json, prepare, queries, r#enum}; /// Apply this attribute to a struct to generate a select query. /// ## Single result /// ```rust,ignore -/// #[select_query( +/// #[read_query( /// query = "select * from person where id = ? limit 1", /// entity_type = "PersonEntity" /// )] @@ -29,9 +19,9 @@ pub(crate) fn token_stream_with_error(mut tokens: TokenStream2, error: syn::Erro /// ``` /// ## Multiple results /// ```rust,ignore -/// #[select_query( +/// #[read_query( /// query = "select * from person where id in ? limit ?", -/// entity_type = "Vec" +/// return_type = "Vec" /// )] /// pub struct GetPeopleByIds { /// pub ids: Vec, @@ -41,13 +31,13 @@ pub(crate) fn token_stream_with_error(mut tokens: TokenStream2, error: syn::Erro /// // -> Vec /// ``` #[proc_macro_attribute] -pub fn select_query(args: TokenStream, input: TokenStream) -> TokenStream { - queries::select::expand(args.into(), input.into()).into() +pub fn read_query(args: TokenStream, input: TokenStream) -> TokenStream { + queries::read::expand(args.into(), input.into()).into() } -/// Apply this attribute to a struct to generate a delete query. +/// Apply this attribute to a struct to generate a write query. /// ```rust,ignore -/// #[delete_query( +/// #[write_query( /// query = "delete from person where id = ?", /// )] /// pub struct DeletePersonById { @@ -55,8 +45,8 @@ pub fn select_query(args: TokenStream, input: TokenStream) -> TokenStream { /// } /// ``` #[proc_macro_attribute] -pub fn delete_query(args: TokenStream, input: TokenStream) -> TokenStream { - queries::delete::expand(args.into(), input.into()).into() +pub fn write_query(args: TokenStream, input: TokenStream) -> TokenStream { + queries::write::expand(args.into(), input.into()).into() } /// Apply this attribute to a entity struct to generate an upsert query. @@ -127,3 +117,8 @@ pub fn int_enum_derive(input: TokenStream) -> TokenStream { pub fn int_enum(args: TokenStream, input: TokenStream) -> TokenStream { r#enum::expand_attr(args.into(), input.into()).into() } + +#[proc_macro] +pub fn create_query_collection(input: TokenStream) -> TokenStream { + prepare::expand(input.into()).into() +} diff --git a/scyllax-macros/src/queries/delete.rs b/scyllax-macros/src/queries/delete.rs deleted file mode 100644 index 792aff2..0000000 --- a/scyllax-macros/src/queries/delete.rs +++ /dev/null @@ -1,65 +0,0 @@ -use darling::{export::NestedMeta, FromMeta}; -use proc_macro2::TokenStream; -use quote::quote; -use syn::ItemStruct; - -use crate::token_stream_with_error; - -#[derive(FromMeta)] -pub(crate) struct SelectQueryOptions { - query: String, - entity_type: syn::Type, -} - -pub(crate) fn expand(args: TokenStream, item: TokenStream) -> TokenStream { - let attr_args = match NestedMeta::parse_meta_list(args.clone()) { - Ok(args) => args, - Err(e) => return darling::Error::from(e).write_errors(), - }; - - let args = match SelectQueryOptions::from_list(&attr_args) { - Ok(o) => o, - Err(e) => return e.write_errors(), - }; - - let entity_type = args.entity_type; - let query = args.query.clone(); - - let input: ItemStruct = match syn::parse2(item.clone()) { - Ok(it) => it, - Err(e) => return token_stream_with_error(item, e), - }; - let struct_ident = &input.ident; - - quote! { - #[derive(scylla::ValueList, std::fmt::Debug, std::clone::Clone, PartialEq, Hash)] - #input - - #[scyllax::async_trait] - impl scyllax::DeleteQuery<#entity_type> for #struct_ident { - fn query() -> String { - #query.to_string() - } - - async fn prepare(db: &Executor) -> Result { - let query = Self::query(); - tracing::debug!{ - target = stringify!(#struct_ident), - query, - "preparing query" - }; - db.session.add_prepared_statement(&scylla::query::Query::new(query)).await - } - - async fn execute(self, db: &scyllax::Executor) -> anyhow::Result { - let query = Self::query(); - tracing::debug! { - query, - "executing delete" - }; - - db.session.execute(query, self).await - } - } - } -} diff --git a/scyllax-macros/src/queries/mod.rs b/scyllax-macros/src/queries/mod.rs deleted file mode 100644 index 3a29e55..0000000 --- a/scyllax-macros/src/queries/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod delete; -pub mod select; -pub mod upsert; diff --git a/scyllax-macros/src/queries/select.rs b/scyllax-macros/src/queries/select.rs deleted file mode 100644 index 030138f..0000000 --- a/scyllax-macros/src/queries/select.rs +++ /dev/null @@ -1,164 +0,0 @@ -use darling::{export::NestedMeta, FromMeta}; -use proc_macro2::TokenStream; -use quote::quote; -use syn::ItemStruct; - -use crate::token_stream_with_error; - -#[derive(FromMeta)] -pub(crate) struct SelectQueryOptions { - query: String, - entity_type: syn::Type, -} - -pub(crate) fn expand(args: TokenStream, item: TokenStream) -> TokenStream { - let attr_args = match NestedMeta::parse_meta_list(args.clone()) { - Ok(args) => args, - Err(e) => return darling::Error::from(e).write_errors(), - }; - - let args = match SelectQueryOptions::from_list(&attr_args) { - Ok(o) => o, - Err(e) => return e.write_errors(), - }; - - let query = args.query.clone(); - let entity_type = args.entity_type; - - let input: ItemStruct = match syn::parse2(item.clone()) { - Ok(it) => it, - Err(e) => return token_stream_with_error(item, e), - }; - let struct_ident = &input.ident; - - // trimmed entity_type - // eg: Vec -> OrgEntity - // eg: OrgEntity -> OrgEntity - let inner_entity_type = if let syn::Type::Path(path) = entity_type.clone() { - let last_segment = path.path.segments.last().unwrap(); - let ident = &last_segment.ident; - - if ident == "Vec" { - let args = &last_segment.arguments; - if let syn::PathArguments::AngleBracketed(args) = args { - let args = &args.args; - if args.len() != 1 { - return token_stream_with_error( - item, - syn::Error::new_spanned( - entity_type, - "entity_type must be a path with one generic argument", - ), - ); - } - - if let syn::GenericArgument::Type(ty) = args.first().unwrap() { - ty.clone() - } else { - return token_stream_with_error( - item, - syn::Error::new_spanned( - entity_type, - "entity_type must be a path with one generic argument", - ), - ); - } - } else { - return token_stream_with_error( - item, - syn::Error::new_spanned( - entity_type, - "entity_type must be a path with one generic argument", - ), - ); - } - } else { - entity_type.clone() - } - } else { - return token_stream_with_error( - item, - syn::Error::new_spanned(entity_type, "entity_type must be a path"), - ); - }; - - // if entity_type is a Vec, return type is Vec - // if entity_type is not a Vec, return type is Option - let return_type = if let syn::Type::Path(path) = entity_type.clone() { - let last_segment = path.path.segments.last().unwrap(); - let ident = &last_segment.ident; - - if ident == "Vec" { - quote! { - #entity_type - } - } else { - quote! { - Option<#entity_type> - } - } - } else { - return token_stream_with_error( - item, - syn::Error::new_spanned(entity_type, "entity_type must be a path"), - ); - }; - - // if entity_type is a Vec, we need to use the macro scyllax:match_rows!(res, entity_type) - // if entity_type is not a Vec, we need to use the macro scyllax:match_row!(res, entity_type) - // eg: Vec -> scyllax:match_rows!(res, OrgEntity) - // eg: OrgEntity -> scyllax:match_row!(res, OrgEntity) - let parser = if let syn::Type::Path(path) = entity_type.clone() { - let last_segment = path.path.segments.last().unwrap(); - let ident = &last_segment.ident; - - if ident == "Vec" { - quote! { - scyllax::match_rows!(res, #inner_entity_type) - } - } else { - quote! { - scyllax::match_row!(res, #path) - } - } - } else { - return token_stream_with_error( - item, - syn::Error::new_spanned(entity_type, "entity_type must be a path"), - ); - }; - - quote! { - #[derive(scylla::ValueList, std::fmt::Debug, std::clone::Clone, PartialEq, Hash)] - #input - - #[scyllax::async_trait] - impl scyllax::SelectQuery<#inner_entity_type, #return_type> for #struct_ident { - fn query() -> String { - #query.replace("*", &#inner_entity_type::keys().join(", ")) - } - - #[tracing::instrument(skip(db))] - async fn prepare(db: &Executor) -> Result { - tracing::debug!("preparing query {}", stringify!(#struct_ident)); - db.session.add_prepared_statement(&scylla::query::Query::new(Self::query())).await - } - - #[tracing::instrument(skip(db))] - async fn execute(self, db: &scyllax::Executor) -> anyhow::Result { - let query = Self::query(); - tracing::debug! { - query, - "executing select" - }; - - db.session.execute(query, self).await - } - - #[tracing::instrument(skip(res))] - async fn parse_response(res: scylla::QueryResult) -> Result<#return_type, scyllax::ScyllaxError> { - #parser - } - } - } -} diff --git a/scyllax-parser/Cargo.toml b/scyllax-parser/Cargo.toml index 60d0b2f..11b7b14 100644 --- a/scyllax-parser/Cargo.toml +++ b/scyllax-parser/Cargo.toml @@ -12,10 +12,10 @@ homepage.workspace = true readme = 'crates.md' [dependencies] -nom = "7.1.3" +nom = "7" [dev-dependencies] -pretty_assertions = "1.4.0" +pretty_assertions = "1" criterion = { version = "0.5", features = ["html_reports"] } [[bench]] diff --git a/scyllax-parser/src/select.rs b/scyllax-parser/src/select.rs index 6091fd7..747c686 100644 --- a/scyllax-parser/src/select.rs +++ b/scyllax-parser/src/select.rs @@ -26,10 +26,11 @@ use nom::{ branch::alt, bytes::complete::{tag, tag_no_case}, - character::complete::{multispace0, multispace1}, - combinator::{map, opt}, + character::complete::{alpha1, alphanumeric1, multispace0, multispace1}, + combinator::{map, opt, recognize}, error::Error, - multi::separated_list0, + multi::{many0_count, separated_list0}, + sequence::pair, Err, IResult, }; @@ -78,6 +79,13 @@ fn parse_asterisk(input: &str) -> IResult<&str, Column> { Ok((input, Column::Asterisk)) } +fn parse_table_name(input: &str) -> IResult<&str, &str> { + recognize(pair( + alt((alpha1, tag("_"))), + many0_count(alt((alphanumeric1, tag("_")))), + ))(input) +} + /// Parses a select query pub fn parse_select(input: &str) -> IResult<&str, SelectQuery> { let (input, _) = tag_no_case("select ")(input)?; @@ -88,7 +96,7 @@ pub fn parse_select(input: &str) -> IResult<&str, SelectQuery> { let (input, _) = multispace1(input)?; let (input, _) = tag_no_case("from ")(input)?; - let (input, table) = parse_identifier(input)?; + let (input, table) = parse_table_name(input)?; let (input, _) = multispace0(input)?; let (input, condition) = opt(parse_where_clause)(input)?; @@ -177,6 +185,28 @@ mod test { assert_eq!(SelectQuery::try_from(query), Ok(res)); } + #[test] + fn test_custom() { + let parsed = parse_select("select * from person_by_email where email = :email limit 1"); + + assert_eq!( + parsed, + Ok(( + "", + SelectQuery { + table: "person_by_email".to_string(), + columns: vec![Column::Asterisk], + condition: vec![WhereClause { + column: Column::Identifier("email".to_string()), + operator: ComparisonOperator::Equal, + value: Value::Variable(Variable::NamedVariable("email".to_string())), + }], + limit: Some(Value::Number(1)), + } + )) + ); + } + #[test] fn test_parse_select() { assert_eq!( diff --git a/src/collection.rs b/src/collection.rs new file mode 100644 index 0000000..8e45c5d --- /dev/null +++ b/src/collection.rs @@ -0,0 +1,18 @@ +use crate::{error::ScyllaxError, executor::GetPreparedStatement, queries::Query}; +use async_trait::async_trait; +use scylla::{prepared_statement::PreparedStatement, Session}; + +/// A collection of prepared statements. +#[async_trait] +pub trait QueryCollection { + async fn new(session: &Session) -> Result + where + Self: Sized; + + fn get_prepared(&self) -> &PreparedStatement + where + Self: GetPreparedStatement, + { + >::get(self) + } +} diff --git a/src/entity.rs b/src/entity.rs new file mode 100644 index 0000000..5852aef --- /dev/null +++ b/src/entity.rs @@ -0,0 +1,10 @@ +use scylla::{frame::value::ValueList, FromRow}; + +/// The traits of the entity +pub trait EntityExt { + /// Returns the keys of the entity as a vector of strings, keeping the order of the keys. + fn keys() -> Vec; + + /// Returns the primary keys + fn pks() -> Vec; +} diff --git a/src/error.rs b/src/error.rs index 579080a..d1c3fb2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -18,6 +18,10 @@ pub enum ScyllaxError { /// There was an error when building an upsert query. #[error("Failed to build query: {0}")] BuildUpsertQueryError(#[from] BuildUpsertQueryError), + + /// An error when serializing values + #[error("Failed to serialize values: {0}")] + SerializedValues(#[from] scylla::frame::value::SerializeValuesError), } /// An error when building an upsert query diff --git a/src/executor.rs b/src/executor.rs index b085ccd..9a71a0c 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -1,82 +1,69 @@ //! The `scyllax` [`Executor`] processes queries. - use crate::{ - error::ScyllaxError, DeleteQuery, EntityExt, FromRow, ImplValueList, SelectQuery, UpsertQuery, -}; -use scylla::{ - prepared_statement::PreparedStatement, query::Query, transport::errors::QueryError, - CachingSession, QueryResult, SessionBuilder, + collection::QueryCollection, + error::ScyllaxError, + prelude::WriteQuery, + queries::{Query, ReadQuery}, }; +use scylla::{prepared_statement::PreparedStatement, QueryResult, Session, SessionBuilder}; /// Creates a new [`CachingSession`] and returns it pub async fn create_session( known_nodes: impl IntoIterator>, default_keyspace: Option>, -) -> anyhow::Result { - let session = CachingSession::from( - SessionBuilder::new() - .known_nodes(known_nodes) - .build() - .await?, - 1_000, - ); +) -> anyhow::Result { + let session = SessionBuilder::new() + .known_nodes(known_nodes) + .build() + .await?; if let Some(ks) = default_keyspace { - session.get_session().use_keyspace(ks, true).await?; + session.use_keyspace(ks, true).await?; } Ok(session) } -/// A structure that executes queries -pub struct Executor { - /// The internal [`scylla::CachingSession`] - pub session: CachingSession, +pub trait GetPreparedStatement { + fn get(&self) -> &PreparedStatement; } -impl Executor { - /// Creates a new [`Executor`] with a provided [`scylla::CachingSession`]. - pub fn with_session(session: CachingSession) -> Executor { - Self { session } - } +pub struct Executor { + pub session: Session, + queries: T, +} - /// Prepares a query - pub async fn prepare_query(&self, query: String) -> Result { - self.session - .add_prepared_statement(&Query::new(query)) - .await - } +impl Executor { + pub async fn new(session: Session) -> Result { + let queries = T::new(&session).await?; - /// Executes a [`SelectQuery`] and returns the result - pub async fn execute_select< - T: EntityExt + FromRow + ImplValueList, - R: Clone + std::fmt::Debug + Send + Sync, - E: SelectQuery, - >( - &self, - query: E, - ) -> Result { - let res = query.execute(self).await?; - E::parse_response(res).await + Ok(Self { session, queries }) } - /// Executes a [`DeleteQuery`] and returns the result - pub async fn execute_delete + FromRow + ImplValueList, E: DeleteQuery>( - &self, - query: E, - ) -> Result { - let res = query.execute(self).await?; + pub async fn execute_read(&self, query: &Q) -> Result + where + Q: Query + ReadQuery, + T: GetPreparedStatement, + { + let statement = self.queries.get_prepared::(); + let variables = query.bind()?; + + let result = self.session.execute(statement, variables).await?; - Ok(res) + Q::parse_response(result).await } - /// Executes a [`UpsertQuery`] and returns the result - pub async fn execute_upsert + FromRow + ImplValueList, E: UpsertQuery>( - &self, - query: E, - ) -> Result { - let res = query.execute(self).await?; + pub async fn execute_write(&self, query: &Q) -> Result + where + Q: Query + WriteQuery, + T: GetPreparedStatement, + { + let statement = self.queries.get_prepared::(); + let variables = query.bind()?; - Ok(res) + self.session + .execute(statement, variables) + .await + .map_err(Into::into) } } diff --git a/src/lib.rs b/src/lib.rs index 2c9852a..d58b90f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ //! ### 2. Select queries //! With the [`select_query`] attribute, it's easy to define select queries. //! ```rust,ignore -//! #[select_query( +//! #[read_query( //! query = "select * from person where id = ? limit 1", //! entity_type = "PersonEntity" //! )] @@ -36,77 +36,13 @@ //! pub created_at: i64, //! } //! ``` -pub use error::BuildUpsertQueryError; -pub use scylla::{ - prepared_statement::PreparedStatement, transport::errors::QueryError, QueryResult, -}; - -pub use crate::{error::ScyllaxError, executor::Executor}; -pub use async_trait::async_trait; -pub use scylla::{frame::value::ValueList as ImplValueList, FromRow}; -pub use scyllax_macros::*; - +pub mod collection; +pub mod entity; pub mod error; pub mod executor; pub mod maybe_unset; +// mod playground; pub mod prelude; +pub mod queries; pub mod rows; pub mod util; - -/// The traits of the entity -pub trait EntityExt { - /// Returns the keys of the entity as a vector of strings, keeping the order of the keys. - fn keys() -> Vec; - - /// Returns the primary keys - fn pks() -> Vec; -} - -/// The trait that's implemented on select/read queries -// R is the return type of the query -// It can be either Option or Vec -#[async_trait] -pub trait SelectQuery< - T: EntityExt + ImplValueList + FromRow, - R: Clone + std::fmt::Debug + Send + Sync, -> -{ - /// Returns the query as a string - fn query() -> String; - - /// Prepares the query - async fn prepare(db: &Executor) -> Result; - - /// Executes the query - async fn execute(self, db: &Executor) -> Result; - - /// Parses the response from the database - async fn parse_response(res: QueryResult) -> Result; -} - -/// The trait that's implemented on update/insert queryes -#[async_trait] -pub trait UpsertQuery + ImplValueList + FromRow> { - /// Returns the query as a string - fn query( - &self, - ) -> Result<(String, scylla::frame::value::SerializedValues), BuildUpsertQueryError>; - - /// Executes the query - async fn execute(self, db: &Executor) -> Result; -} - -/// The trait that's implemented on delete queries -// R is the return type of the query -// It can be either Option or Vec -#[async_trait] -pub trait DeleteQuery + ImplValueList + FromRow> { - /// Returns the query as a string - fn query() -> String; - - /// Prepares the query - async fn prepare(db: &Executor) -> Result; - - /// Executes the query - async fn execute(self, db: &Executor) -> Result; -} diff --git a/src/playground.rs b/src/playground.rs new file mode 100644 index 0000000..380c03e --- /dev/null +++ b/src/playground.rs @@ -0,0 +1,164 @@ +//! playground +// Well. +// We have a wrapping struct +// RawDatabase +// So you deal with a RawDatabase +// And T is the queries you can execute. +// Then you can trait bound on T +// Which gives you compile time check to see that your query is in the struct because that implies it has a Get trait impl on T. +use crate::error::ScyllaxError; +use async_trait::async_trait; +use scylla::{ + frame::value::{SerializeValuesError, SerializedValues}, + prepared_statement::PreparedStatement, + Session, SessionBuilder, +}; +#[allow(unused, dead_code, unused_variables)] +use std::{future::Future, pin::Pin}; + +type Result = std::result::Result; +type SerializedValuesResult = std::result::Result; + +/// the entity +struct UserEntity; + +/// generic query implement. this implements on all queries. +trait Query { + fn query() -> String; + + fn bind(&self) -> SerializedValuesResult; +} + +/// implements on read queries, which return an output. +trait ReadQuery { + type Output; +} + +/// empty query implementation for all write queries. this is just a marker trait. +/// so you cant pass a write query into a read query function. +trait WriteQuery {} + +trait GetPreparedStatement { + fn get(&self) -> &PreparedStatement; +} + +type BoxFuture<'a, T> = Pin + Send + 'a>>; + +/// A collection of prepared statements. +#[async_trait] +trait QueryCollection { + async fn new(session: &Session) -> Result + where + Self: Sized; + + fn get_prepared(&self) -> &PreparedStatement + where + Self: GetPreparedStatement, + { + >::get(self) + } +} + +struct UserByIdQuery { + id: i32, +} +impl Query for UserByIdQuery { + fn query() -> String { + "select * from users where id = :id".to_string() + } + + fn bind(&self) -> SerializedValuesResult { + let mut values = SerializedValues::new(); + values.add_named_value("id", &self.id)?; + + Ok(values) + } +} +impl ReadQuery for UserByIdQuery { + type Output = UserEntity; +} +impl GetPreparedStatement for UserQueries { + fn get(&self) -> &PreparedStatement { + &self.user_by_id_query + } +} + +struct UserByEmailQuery { + email: String, +} +impl Query for UserByEmailQuery { + fn query() -> String { + "select * from users_by_email where email = :email".to_string() + } + + fn bind(&self) -> SerializedValuesResult { + let mut values = SerializedValues::with_capacity(1); + values.add_named_value("email", &self.email); + + Ok(values) + } +} +impl ReadQuery for UserByEmailQuery { + type Output = UserEntity; +} +impl GetPreparedStatement for UserQueries { + fn get(&self) -> &PreparedStatement { + &self.user_by_email_query + } +} + +#[allow(nonstandard_style, non_snake_case)] +struct UserQueries { + user_by_id_query: PreparedStatement, + user_by_email_query: PreparedStatement, +} + +#[async_trait] +impl QueryCollection for UserQueries { + async fn new(session: &Session) -> Result { + Ok(Self { + user_by_id_query: session.prepare(UserByIdQuery::query()).await?, + user_by_email_query: session.prepare(UserByEmailQuery::query()).await?, + }) + } +} + +struct Executor { + session: Session, + queries: T, +} + +impl Executor { + async fn new(session: Session) -> Result { + let queries = T::new(&session).await?; + + Ok(Self { session, queries }) + } + + async fn execute_read(&self, query: &Q) -> Result + where + Q: Query + ReadQuery, + T: GetPreparedStatement, + { + let statement = self.queries.get_prepared::(); + let variables = query.bind()?; + + let result = self.session.execute(statement, variables).await?; + + todo!("execute the query") + } +} + +async fn test() -> Result<()> { + let session = SessionBuilder::new().build().await.unwrap(); + + let queries = Executor::::new(session).await.unwrap(); + + let user = queries + .execute_read(&UserByEmailQuery { + email: "foo@bar.com".to_string(), + }) + .await?; + + Ok(()) +} diff --git a/src/prelude.rs b/src/prelude.rs index add5f2b..775c9ba 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,10 +1,20 @@ //! Re-exports of the most commonly used types and traits. pub use crate::{ - delete_query, entity, error::BuildUpsertQueryError, executor::Executor, int_enum, json_data, - maybe_unset::MaybeUnset, select_query, upsert_query, util::v1_uuid, DeleteQuery, Entity, - EntityExt, FromRow, ImplValueList, IntEnum, JsonData, ScyllaxError, SelectQuery, UpsertQuery, + collection::QueryCollection, + entity::EntityExt, + error::{BuildUpsertQueryError, ScyllaxError}, + executor::{create_session, Executor, GetPreparedStatement}, + maybe_unset::MaybeUnset, + queries::{Query, ReadQuery, SerializedValuesResult, WriteQuery}, + util::v1_uuid, }; +pub use async_trait::async_trait; +pub use scyllax_macros::*; -pub use scylla::frame::value::SerializeValuesError; -pub use scylla::frame::value::SerializedValues; -pub use scylla::ValueList; +pub mod scylla_reexports { + //! Re-exports of the most commonly used types and traits from the `scylla` crate. + pub use scylla::{ + frame::value, statement::prepared_statement::PreparedStatement, + transport::errors::QueryError, FromRow, QueryResult, Session, ValueList, + }; +} diff --git a/src/queries.rs b/src/queries.rs new file mode 100644 index 0000000..c48de11 --- /dev/null +++ b/src/queries.rs @@ -0,0 +1,30 @@ +use crate::error::ScyllaxError; +use async_trait::async_trait; +use scylla::{ + frame::value::{SerializeValuesError, SerializedValues}, + QueryResult, +}; + +pub type SerializedValuesResult = std::result::Result; + +/// A generic query implement. This implements on all queries for type-safety. +pub trait Query { + /// Returns the query as a string + fn query() -> String; + + /// Turns the query into a [`SerializedValues`] + fn bind(&self) -> SerializedValuesResult; +} + +/// The trait that's implemented on read queries, which return an output which demands a parser. +#[async_trait] +pub trait ReadQuery { + type Output: Clone + std::fmt::Debug + Send + Sync; + + /// Parses the response from the database + async fn parse_response(rows: QueryResult) -> Result; +} + +/// Empty query implementation for all write queries. This is just a marker trait. +/// So you cant pass a write query into a read query function. +pub trait WriteQuery {} diff --git a/src/rows.rs b/src/rows.rs index 268c022..a9d0b18 100644 --- a/src/rows.rs +++ b/src/rows.rs @@ -16,7 +16,7 @@ macro_rules! match_row { SingleRowTypedError::BadNumberOfRows(_) => Ok(None), _ => { tracing::error!("err: {:?}", err); - Err(scyllax::ScyllaxError::SingleRowTyped(err)) + Err(scyllax::prelude::ScyllaxError::SingleRowTyped(err)) } } }