diff --git a/Cargo.lock b/Cargo.lock index 8ff0734..96884c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -477,7 +477,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "es-entity" -version = "0.10.34-dev" +version = "0.10.34" dependencies = [ "anyhow", "async-graphql", @@ -505,7 +505,7 @@ dependencies = [ [[package]] name = "es-entity-macros" -version = "0.10.34-dev" +version = "0.10.34" dependencies = [ "convert_case", "darling 0.23.0", @@ -1000,9 +1000,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.184" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libm" @@ -1076,9 +1076,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mio" -version = "1.2.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", @@ -1762,12 +1762,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2121,9 +2121,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.51.1" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -2136,9 +2136,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -2321,9 +2321,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.23.0" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ "getrandom 0.4.1", "js-sys", @@ -2573,6 +2573,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -2606,13 +2615,30 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -2625,6 +2651,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -2637,6 +2669,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -2649,12 +2687,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -2667,6 +2717,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -2679,6 +2735,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -2691,6 +2753,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -2703,6 +2771,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "0.7.14" diff --git a/Cargo.toml b/Cargo.toml index 6ef2f26..af2632d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "es-entity" description = "Event Sourcing Entity Framework" repository = "https://github.com/GaloyMoney/es-entity" documentation = "https://docs.rs/es-entity" -version = "0.10.34-dev" +version = "0.10.34" edition = "2024" license = "Apache-2.0" categories = ["data-structures", "database"] @@ -62,7 +62,7 @@ members = [ [workspace.dependencies] -es-entity-macros = { path = "es-entity-macros", version = "0.10.34-dev" } +es-entity-macros = { path = "es-entity-macros", version = "0.10.34" } anyhow = "1.0" async-graphql = { version = "8.0.0-rc.3", default-features = false } @@ -75,9 +75,9 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_with = "3.15" sqlx = { version = "0.8", default-features = false, features = ["macros", "runtime-tokio-rustls", "postgres", "uuid", "chrono", "json" ] } -tokio = { version = "1.51", features = ["rt-multi-thread", "macros", "time"] } +tokio = { version = "1.50", features = ["rt-multi-thread", "macros", "time"] } thiserror = "2.0" -uuid = { version = "1.23", features = ["serde", "v7"] } +uuid = { version = "1.22", features = ["serde", "v7"] } im = { version = "15.1", features = ["serde"] } pin-project = "1.1" tracing = { version = "0.1.41", default-features = false } diff --git a/es-entity-macros/Cargo.toml b/es-entity-macros/Cargo.toml index 97ab3aa..2dbe4f6 100644 --- a/es-entity-macros/Cargo.toml +++ b/es-entity-macros/Cargo.toml @@ -2,7 +2,7 @@ name = "es-entity-macros" description = "Proc macros for es-entity" repository = "https://github.com/GaloyMoney/cala" -version = "0.10.34-dev" +version = "0.10.34" edition = "2024" license = "Apache-2.0" categories = ["data-structures", "database"] diff --git a/es-entity-macros/src/repo/list_for_filters_fn.rs b/es-entity-macros/src/repo/list_for_filters_fn.rs index 1e059b7..93dea46 100644 --- a/es-entity-macros/src/repo/list_for_filters_fn.rs +++ b/es-entity-macros/src/repo/list_for_filters_fn.rs @@ -73,7 +73,7 @@ impl ToTokens for FiltersStruct<'_> { let fields = self.fields(); tokens.append_all(quote! { - #[derive(Debug, Default)] + #[derive(Debug, Default, Clone)] pub struct #ident { #fields } @@ -95,6 +95,7 @@ pub struct ListForFiltersFn<'a> { id: &'a syn::Ident, any_nested: bool, post_hydrate_error: Option<&'a syn::Type>, + count: bool, #[cfg(feature = "instrument")] repo_name_snake: String, } @@ -120,6 +121,7 @@ impl<'a> ListForFiltersFn<'a> { id: opts.id(), any_nested: opts.any_nested(), post_hydrate_error: opts.post_hydrate_hook.as_ref().map(|h| &h.error), + count: opts.count, #[cfg(feature = "instrument")] repo_name_snake: opts.repo_name_snake_case(), } @@ -618,6 +620,131 @@ impl ToTokens for ListForFiltersFn<'_> { break; } } + + // Generate count_for_filters and count methods (opt-in via #[es_repo(count)]) + if self.count { + self.generate_count_fns(tokens); + } + } +} + +impl ListForFiltersFn<'_> { + fn generate_count_fns(&self, tokens: &mut TokenStream) { + let filters_name = self.filters_struct.ident(); + let error = &self.query_error; + + let where_fragments: Vec = self + .for_columns + .iter() + .enumerate() + .map(|(i, col)| FiltersStruct::where_clause_fragment(col, (i + 1) as u32)) + .collect(); + + let not_deleted = self.delete.not_deleted_condition(); + + let filter_where = if where_fragments.is_empty() { + String::new() + } else { + format!("WHERE {}", where_fragments.join(" AND ")) + }; + + let full_where = if filter_where.is_empty() { + if not_deleted.is_empty() { + String::new() + } else { + "WHERE deleted = FALSE".to_string() + } + } else if not_deleted.is_empty() { + filter_where + } else { + format!("{filter_where}{not_deleted}") + }; + + let count_query = if full_where.is_empty() { + format!( + r#"SELECT COUNT(*) as "count!" FROM {}"#, + self.table_name, + ) + } else { + format!( + r#"SELECT COUNT(*) as "count!" FROM {} {}"#, + self.table_name, full_where, + ) + }; + + let destructure_filters: TokenStream = self + .for_columns + .iter() + .map(|c| { + let col_name = c.name(); + let filter_name = + syn::Ident::new(&format!("filter_{}", col_name), Span::call_site()); + quote! { + let #filter_name = filters.#col_name; + } + }) + .collect(); + + let filter_arg_bindings: TokenStream = self + .for_columns + .iter() + .map(|col| FiltersStruct::filter_arg_tokens(col)) + .collect(); + + #[cfg(feature = "instrument")] + let (count_instrument_attr, count_error_recording) = { + let entity_name = self.entity.to_string(); + let repo_name = &self.repo_name_snake; + let span_name = format!("{}.count_for_filters", repo_name); + ( + quote! { + #[tracing::instrument(name = #span_name, skip_all, fields(entity = #entity_name, filters = tracing::field::debug(&filters), count = tracing::field::Empty, error = tracing::field::Empty, exception.message = tracing::field::Empty, exception.type = tracing::field::Empty))] + }, + quote! { + if let Err(ref e) = __result { + tracing::Span::current().record("error", true); + tracing::Span::current().record("exception.message", tracing::field::display(e)); + tracing::Span::current().record("exception.type", std::any::type_name_of_val(e)); + } + }, + ) + }; + #[cfg(not(feature = "instrument"))] + let (count_instrument_attr, count_error_recording) = (quote! {}, quote! {}); + + #[cfg(feature = "instrument")] + let count_record_result = quote! { + tracing::Span::current().record("count", count); + }; + #[cfg(not(feature = "instrument"))] + let count_record_result = quote! {}; + + tokens.append_all(quote! { + #count_instrument_attr + pub async fn count_for_filters( + &self, + filters: #filters_name, + ) -> Result { + let __result: Result = async { + #destructure_filters + let count: i64 = sqlx::query_scalar!( + #count_query, + #filter_arg_bindings + ) + .fetch_one(self.pool()) + .await?; + #count_record_result + Ok(count) + }.await; + + #count_error_recording + __result + } + + pub async fn count(&self) -> Result { + self.count_for_filters(Default::default()).await + } + }); } } @@ -645,7 +772,7 @@ mod tests { filters.to_tokens(&mut tokens); let expected = quote! { - #[derive(Debug, Default)] + #[derive(Debug, Default, Clone)] pub struct OrderFilters { pub customer_id: Option, pub status: Option, @@ -701,6 +828,7 @@ mod tests { id: &id, any_nested: false, post_hydrate_error: None, + count: true, #[cfg(feature = "instrument")] repo_name_snake: "test_repo".to_string(), }; @@ -820,6 +948,32 @@ mod tests { __result } + + + + pub async fn count_for_filters( + &self, + filters: OrderFilters, + ) -> Result { + let __result: Result = async { + let filter_customer_id = filters.customer_id; + let filter_status = filters.status; + let count: i64 = sqlx::query_scalar!( + "SELECT COUNT(*) as \"count!\" FROM orders WHERE COALESCE(customer_id = $1, $1 IS NULL) AND COALESCE(status = $2, $2 IS NULL)", + filter_customer_id as Option, + filter_status as Option, + ) + .fetch_one(self.pool()) + .await?; + Ok(count) + }.await; + + __result + } + + pub async fn count(&self) -> Result { + self.count_for_filters(Default::default()).await + } }; assert_eq!(tokens.to_string(), expected.to_string()); @@ -872,6 +1026,7 @@ mod tests { id: &id, any_nested: false, post_hydrate_error: None, + count: true, #[cfg(feature = "instrument")] repo_name_snake: "test_repo".to_string(), }; @@ -939,6 +1094,7 @@ mod tests { id: &id, any_nested: false, post_hydrate_error: None, + count: true, #[cfg(feature = "instrument")] repo_name_snake: "test_repo".to_string(), }; diff --git a/es-entity-macros/src/repo/options/mod.rs b/es-entity-macros/src/repo/options/mod.rs index 8f0670e..58d56af 100644 --- a/es-entity-macros/src/repo/options/mod.rs +++ b/es-entity-macros/src/repo/options/mod.rs @@ -205,6 +205,8 @@ pub struct RepositoryOptions { pub post_hydrate_hook: Option, #[darling(default)] pub delete: DeleteOption, + #[darling(default)] + pub count: bool, data: darling::ast::Data<(), RepoField>,