diff --git a/README.md b/README.md index 6608eca..e3cd469 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Georm is a lightweight, opinionated Object-Relational Mapping (ORM) library buil ### Key Features - **Type Safety**: Compile-time verified SQL queries using SQLx macros +- **Flexible Executor**: Works with both `PgPool` and `Transaction` for atomic operations. - **Zero Runtime Cost**: No reflection or runtime query building - **Simple API**: Intuitive derive macros for common operations - **Relationship Support**: One-to-one, one-to-many, and many-to-many relationships @@ -116,13 +117,16 @@ pub struct Post { use sqlx::PgPool; async fn example(pool: &PgPool) -> sqlx::Result<()> { + // Start a transaction + let mut tx = pool.begin().await?; + // Create an author let author = Author { id: 0, // Will be auto-generated name: "Jane Doe".to_string(), email: "jane@example.com".to_string(), }; - let author = author.create(pool).await?; + let author = author.create(&mut *tx).await?; // Create a post let post = Post { @@ -133,9 +137,12 @@ async fn example(pool: &PgPool) -> sqlx::Result<()> { author_id: author.id, created_at: chrono::Utc::now(), }; - let post = post.create(pool).await?; + let post = post.create(&mut *tx).await?; - // Find all posts + // Commit the transaction + tx.commit().await?; + + // Find all posts (using the pool directly) let all_posts = Post::find_all(pool).await?; // Get the post's author @@ -521,22 +528,41 @@ pub struct Post { ### Core Operations -All entities implementing `Georm` get these methods: +All entities implementing `Georm` get these methods. All database operations now accept any `sqlx`-compatible executor, which can be a connection pool (`&PgPool`) or a transaction (`&mut Transaction<'_, Postgres>`). + +```rust +async fn example(pool: &PgPool, post_id: i32) -> sqlx::Result<()> { + // Operations on the pool + let all_posts = Post::find_all(pool).await?; + + // Operations within a transaction + let mut tx = pool.begin().await?; + let post = Post::find(&mut *tx, &post_id).await?; + if let Some(post) = post { + post.delete(&mut *tx).await?; + } + tx.commit().await?; + + Ok(()) +} +``` + +The available methods are: ```rust // Query operations -Post::find_all(pool).await?; // Find all posts -Post::find(pool, &post_id).await?; // Find by ID +Post::find_all(executor).await?; +Post::find(executor, &post_id).await?; // Mutation operations -post.create(pool).await?; // Insert new record -post.update(pool).await?; // Update existing record -post.create_or_update(pool).await?; // Upsert operation -post.delete(pool).await?; // Delete this record -Post::delete_by_id(pool, &post_id).await?; // Delete by ID +post.create(executor).await?; +post.update(executor).await?; +post.create_or_update(executor).await?; +post.delete(executor).await?; +Post::delete_by_id(executor, &post_id).await?; // Utility -post.get_id(); // Get entity ID +post.get_id(); ``` ### Defaultable Operations @@ -624,7 +650,6 @@ cargo run help # For a list of all available actions ## Roadmap ### High Priority -- **Transaction Support**: Comprehensive transaction handling with atomic operations - **Simplified Relationship Syntax**: Remove redundant table/remote_id specifications by inferring them from target entity metadata - **Multi-Database Support**: MySQL and SQLite support with feature flags diff --git a/examples/postgres/users-comments-and-followers/src/cli/comments.rs b/examples/postgres/users-comments-and-followers/src/cli/comments.rs index 0f7bad9..86b2f0d 100644 --- a/examples/postgres/users-comments-and-followers/src/cli/comments.rs +++ b/examples/postgres/users-comments-and-followers/src/cli/comments.rs @@ -61,7 +61,8 @@ async fn create_comment( pool: &sqlx::PgPool, ) -> Result { let prompt = "Who is creating the comment?"; - let user = User::get_user_by_username_or_select(username.as_deref(), prompt, pool).await?; + let mut tx = pool.begin().await?; + let user = User::get_user_by_username_or_select(username.as_deref(), prompt, &mut *tx).await?; let content = match text { Some(text) => text, None => inquire::Text::new("Content of the comment:") @@ -73,29 +74,33 @@ async fn create_comment( content, id: None, }; - let comment = comment.create(pool).await?; + let comment = comment.create(&mut *tx).await?; + tx.commit().await?; println!("Successfuly created comment:\n{comment}"); Ok(()) } async fn remove_comment(id: Option, pool: &sqlx::PgPool) -> Result { let prompt = "Select the comment to remove:"; + let mut tx = pool.begin().await?; let comment = match id { - Some(id) => Comment::find(pool, &id) + Some(id) => Comment::find(&mut *tx, &id) .await .map_err(UserInputError::DatabaseError)? .ok_or(UserInputError::CommentDoesNotExist)?, - None => Comment::select_comment(prompt, pool).await?, + None => Comment::select_comment(prompt, &mut *tx).await?, }; - comment.delete(pool).await?; + comment.delete(&mut *tx).await?; + tx.commit().await?; Ok(()) } async fn remove_user_comment(username: Option, pool: &sqlx::PgPool) -> Result { + let mut tx = pool.begin().await?; let prompt = "Select user whose comment you want to delete:"; - let user = User::get_user_by_username_or_select(username.as_deref(), prompt, pool).await?; + let user = User::get_user_by_username_or_select(username.as_deref(), prompt, &mut *tx).await?; let comments: HashMap = user - .get_comments(pool) + .get_comments(&mut *tx) .await? .into_iter() .map(|comment| (comment.content.clone(), comment)) @@ -105,7 +110,8 @@ async fn remove_user_comment(username: Option, pool: &sqlx::PgPool) -> R .prompt() .map_err(UserInputError::InquireError)?; let comment: &Comment = comments.get(&selected_comment_content).unwrap(); - comment.delete(pool).await?; + comment.delete(&mut *tx).await?; + tx.commit().await?; Ok(()) } diff --git a/examples/postgres/users-comments-and-followers/src/cli/followers.rs b/examples/postgres/users-comments-and-followers/src/cli/followers.rs index d23ea19..23a2281 100644 --- a/examples/postgres/users-comments-and-followers/src/cli/followers.rs +++ b/examples/postgres/users-comments-and-followers/src/cli/followers.rs @@ -53,16 +53,17 @@ async fn follow_user( followed: Option, pool: &sqlx::PgPool, ) -> Result { + let mut tx = pool.begin().await?; let follower = User::get_user_by_username_or_select( follower.as_deref(), "Select who will be following someone:", - pool, + &mut *tx, ) .await?; let followed = User::get_user_by_username_or_select( followed.as_deref(), "Select who will be followed:", - pool, + &mut *tx, ) .await?; let follow = FollowerDefault { @@ -70,17 +71,22 @@ async fn follow_user( follower: follower.id, followed: followed.id, }; - follow.create(pool).await?; + follow.create(&mut *tx).await?; + tx.commit().await?; println!("User {follower} now follows {followed}"); Ok(()) } async fn unfollow_user(follower: Option, pool: &sqlx::PgPool) -> Result { - let follower = - User::get_user_by_username_or_select(follower.as_deref(), "Select who is following", pool) - .await?; + let mut tx = pool.begin().await?; + let follower = User::get_user_by_username_or_select( + follower.as_deref(), + "Select who is following", + &mut *tx, + ) + .await?; let followed_list: HashMap = follower - .get_followed(pool) + .get_followed(&mut *tx) .await? .iter() .map(|person| (person.username.clone(), person.clone())) @@ -97,8 +103,9 @@ async fn unfollow_user(follower: Option, pool: &sqlx::PgPool) -> Result follower.id, followed.id ) - .execute(pool) + .execute(&mut *tx) .await?; + tx.commit().await?; println!("User {follower} unfollowed {followed}"); Ok(()) } diff --git a/examples/postgres/users-comments-and-followers/src/models/comments.rs b/examples/postgres/users-comments-and-followers/src/models/comments.rs index 3965205..519e561 100644 --- a/examples/postgres/users-comments-and-followers/src/models/comments.rs +++ b/examples/postgres/users-comments-and-followers/src/models/comments.rs @@ -18,8 +18,11 @@ pub struct Comment { } impl Comment { - pub async fn select_comment(prompt: &str, pool: &sqlx::PgPool) -> Result { - let comments: HashMap = Self::find_all(pool) + pub async fn select_comment<'e, E>(prompt: &str, executor: E) -> Result + where + E: sqlx::Executor<'e, Database = sqlx::Postgres>, + { + let comments: HashMap = Self::find_all(executor) .await? .into_iter() .map(|comment| (comment.content.clone(), comment)) diff --git a/examples/postgres/users-comments-and-followers/src/models/profiles.rs b/examples/postgres/users-comments-and-followers/src/models/profiles.rs index a0562aa..0ff3be7 100644 --- a/examples/postgres/users-comments-and-followers/src/models/profiles.rs +++ b/examples/postgres/users-comments-and-followers/src/models/profiles.rs @@ -38,7 +38,10 @@ impl Profile { self.bio.clone().unwrap_or_default() } - pub async fn try_new(user_id: i32, pool: &sqlx::PgPool) -> Result { + pub async fn try_new<'e, E>(user_id: i32, executor: E) -> Result + where + E: sqlx::Executor<'e, Database = sqlx::Postgres>, + { let profile = ProfileDefault { user_id, id: None, @@ -46,20 +49,23 @@ impl Profile { display_name: None, }; profile - .create(pool) + .create(executor) .await .map_err(UserInputError::DatabaseError) } - pub async fn update_interactive( + pub async fn update_interactive<'e, E>( &mut self, display_name: Option, bio: Option, - pool: &sqlx::PgPool, - ) -> Result { + executor: E + ) -> Result + where + E: sqlx::Executor<'e, Database = sqlx::Postgres>, + { self.display_name = display_name; self.bio = bio; - self.update(pool) + self.update(executor) .await .map_err(UserInputError::DatabaseError) } diff --git a/examples/postgres/users-comments-and-followers/src/models/users.rs b/examples/postgres/users-comments-and-followers/src/models/users.rs index 5c695af..650aaef 100644 --- a/examples/postgres/users-comments-and-followers/src/models/users.rs +++ b/examples/postgres/users-comments-and-followers/src/models/users.rs @@ -50,8 +50,11 @@ impl From<&str> for UserDefault { } impl User { - async fn select_user(prompt: &str, pool: &sqlx::PgPool) -> Result { - let users: HashMap = Self::find_all(pool) + async fn select_user<'e, E>(prompt: &str, executor: E) -> Result + where + E: sqlx::Executor<'e, Database = sqlx::Postgres>, + { + let users: HashMap = Self::find_all(executor) .await? .into_iter() .map(|user| (user.username.clone(), user)) @@ -63,41 +66,50 @@ impl User { Ok(user.clone()) } - pub async fn get_user_by_id_or_select( + pub async fn get_user_by_id_or_select<'e, E>( id: Option, prompt: &str, - pool: &sqlx::PgPool, - ) -> Result { + executor: E + ) -> Result + where + E: sqlx::Executor<'e, Database = sqlx::Postgres>, + { let user = match id { - Some(id) => Self::find(pool, &id) + Some(id) => Self::find(executor, &id) .await? .ok_or(UserInputError::UserDoesNotExist)?, - None => Self::select_user(prompt, pool).await?, + None => Self::select_user(prompt, executor).await?, }; Ok(user) } - pub async fn get_user_by_username_or_select( + pub async fn get_user_by_username_or_select<'e, E>( username: Option<&str>, prompt: &str, - pool: &sqlx::PgPool, - ) -> Result { + executor: E, + ) -> Result + where + E: sqlx::Executor<'e, Database = sqlx::Postgres>, + { let user = match username { - Some(username) => Self::find_by_username(username, pool) + Some(username) => Self::find_by_username(username, executor) .await? .ok_or(UserInputError::UserDoesNotExist)?, - None => Self::select_user(prompt, pool).await?, + None => Self::select_user(prompt, executor).await?, }; Ok(user) } - pub async fn find_by_username(username: &str, pool: &sqlx::PgPool) -> Result> { + pub async fn find_by_username<'e, E>(username: &str, executor: E) -> Result> + where + E: sqlx::Executor<'e, Database = sqlx::Postgres>, + { sqlx::query_as!( Self, "SELECT * FROM Users u WHERE u.username = $1", username ) - .fetch_optional(pool) + .fetch_optional(executor) .await .map_err(UserInputError::DatabaseError) } @@ -116,7 +128,8 @@ impl User { Ok(user) } - pub async fn update_profile(id: Option, pool: &sqlx::PgPool) -> Result<(User, Profile)> { + pub async fn update_profile(id: Option, pool: &sqlx::PgPool) -> Result<(User, Profile)> + { let prompt = "Select the user whose profile you want to update"; let user = Self::get_user_by_id_or_select(id, prompt, pool).await?; let profile = match user.get_profile(pool).await? { diff --git a/georm-macros/src/georm/defaultable_struct.rs b/georm-macros/src/georm/defaultable_struct.rs index 0402d5a..135cee8 100644 --- a/georm-macros/src/georm/defaultable_struct.rs +++ b/georm-macros/src/georm/defaultable_struct.rs @@ -94,7 +94,10 @@ fn generate_defaultable_trait_impl( quote! { impl ::georm::Defaultable<#id_type, #struct_name> for #defaultable_struct_name { - async fn create(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<#struct_name> { + async fn create<'e, E>(&self, mut executor: E) -> ::sqlx::Result<#struct_name> + where + E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres> + { let mut dynamic_fields = Vec::new(); #(#field_checks)* @@ -121,7 +124,7 @@ fn generate_defaultable_trait_impl( // Then bind defaultable fields that have values #(#bind_checks)* - query_builder.fetch_one(pool).await + query_builder.fetch_one(executor).await } } } diff --git a/georm-macros/src/georm/ir/m2m_relationship.rs b/georm-macros/src/georm/ir/m2m_relationship.rs index dc876cc..b6c6e10 100644 --- a/georm-macros/src/georm/ir/m2m_relationship.rs +++ b/georm-macros/src/georm/ir/m2m_relationship.rs @@ -71,8 +71,11 @@ WHERE local.{} = $1", value.local.id ); quote! { - pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result> { - ::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(pool).await + pub async fn #function<'e, E>(&self, mut executor: E) -> ::sqlx::Result> + where + E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres> + { + ::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(executor).await } } } diff --git a/georm-macros/src/georm/ir/mod.rs b/georm-macros/src/georm/ir/mod.rs index 9d6876c..aef2803 100644 --- a/georm-macros/src/georm/ir/mod.rs +++ b/georm-macros/src/georm/ir/mod.rs @@ -172,8 +172,11 @@ impl From<&GeormField> for proc_macro2::TokenStream { quote! { fetch_one } }; quote! { - pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<#return_type> { - ::sqlx::query_as!(#entity, #query, self.#local_ident).#fetch(pool).await + pub async fn #function<'e, E>(&self, mut executor: E) -> ::sqlx::Result<#return_type> + where + E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres> + { + ::sqlx::query_as!(#entity, #query, self.#local_ident).#fetch(executor).await } } } diff --git a/georm-macros/src/georm/ir/simple_relationship.rs b/georm-macros/src/georm/ir/simple_relationship.rs index 5046068..6dfa254 100644 --- a/georm-macros/src/georm/ir/simple_relationship.rs +++ b/georm-macros/src/georm/ir/simple_relationship.rs @@ -45,8 +45,11 @@ impl From<&SimpleRelationship> for proc_macro2::TokenStream { let entity = &value.entity; let function = value.make_function_name(); quote! { - pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result> { - ::sqlx::query_as!(#entity, #query, self.get_id()).fetch_optional(pool).await + pub async fn #function<'e, E>(&self, mut executor: E) -> ::sqlx::Result> + where + E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres> + { + ::sqlx::query_as!(#entity, #query, self.get_id()).fetch_optional(executor).await } } } @@ -58,8 +61,11 @@ impl From<&SimpleRelationship> for proc_macro2::TokenStream { let entity = &value.entity; let function = value.make_function_name(); quote! { - pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result> { - ::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(pool).await + pub async fn #function<'e, E>(&self, mut executor: E) -> ::sqlx::Result> + where + E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres> + { + ::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(executor).await } } } diff --git a/georm-macros/src/georm/traits/create.rs b/georm-macros/src/georm/traits/create.rs index 7d3cd36..810bebc 100644 --- a/georm-macros/src/georm/traits/create.rs +++ b/georm-macros/src/georm/traits/create.rs @@ -21,13 +21,16 @@ pub fn generate_create_query(table_name: &str, fields: &[GeormField]) -> proc_ma placeholders.join(", ") ); quote! { - async fn create(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { + async fn create<'e, E>(&self, mut executor: E) -> ::sqlx::Result + where + E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres> + { ::sqlx::query_as!( Self, #query, #(self.#field_idents),* ) - .fetch_one(pool) + .fetch_one(executor) .await } } diff --git a/georm-macros/src/georm/traits/delete.rs b/georm-macros/src/georm/traits/delete.rs index 7cc4ee7..253c5be 100644 --- a/georm-macros/src/georm/traits/delete.rs +++ b/georm-macros/src/georm/traits/delete.rs @@ -24,16 +24,22 @@ pub fn generate_delete_query(table: &str, id: &IdType) -> proc_macro2::TokenStre }; let delete_string = format!("DELETE FROM {table} WHERE {where_clause}"); quote! { - async fn delete_by_id(pool: &::sqlx::PgPool, id: &#id_type) -> ::sqlx::Result { + async fn delete<'e, E>(&self, mut executor: E) -> ::sqlx::Result + where + E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres> + { + Self::delete_by_id(executor, &self.get_id()).await + } + + async fn delete_by_id<'e, E>(mut executor: E, id: &#id_type) -> ::sqlx::Result + where + E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres> + { let rows_affected = ::sqlx::query!(#delete_string, #query_args) - .execute(pool) + .execute(executor) .await? .rows_affected(); Ok(rows_affected) } - - async fn delete(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { - Self::delete_by_id(pool, &self.get_id()).await - } } } diff --git a/georm-macros/src/georm/traits/find.rs b/georm-macros/src/georm/traits/find.rs index c47e9f8..92d1f8f 100644 --- a/georm-macros/src/georm/traits/find.rs +++ b/georm-macros/src/georm/traits/find.rs @@ -4,8 +4,11 @@ use quote::quote; pub fn generate_find_all_query(table: &str) -> proc_macro2::TokenStream { let find_string = format!("SELECT * FROM {table}"); quote! { - async fn find_all(pool: &::sqlx::PgPool) -> ::sqlx::Result> { - ::sqlx::query_as!(Self, #find_string).fetch_all(pool).await + async fn find_all<'e, E>(mut executor: E) -> ::sqlx::Result> + where + E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres> + { + ::sqlx::query_as!(Self, #find_string).fetch_all(executor).await } } } @@ -18,10 +21,13 @@ pub fn generate_find_query(table: &str, id: &IdType) -> proc_macro2::TokenStream } => { let find_string = format!("SELECT * FROM {table} WHERE {} = $1", field_name); quote! { - async fn find(pool: &::sqlx::PgPool, id: &#field_type) -> ::sqlx::Result> { + async fn find<'e, E>(mut executor: E, id: &#field_type) -> ::sqlx::Result> + where + E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres> + { ::sqlx::query_as!(Self, #find_string, id) - .fetch_optional(pool) - .await + .fetch_optional(executor) + .await } } } @@ -36,10 +42,13 @@ pub fn generate_find_query(table: &str, id: &IdType) -> proc_macro2::TokenStream fields.iter().map(|field| field.name.clone()).collect(); let find_string = format!("SELECT * FROM {table} WHERE {id_match_string}"); quote! { - async fn find(pool: &::sqlx::PgPool, id: &#field_type) -> ::sqlx::Result> { + async fn find<'e, E>(mut executor: E, id: &#field_type) -> ::sqlx::Result> + where + E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres> + { ::sqlx::query_as!(Self, #find_string, #(id.#id_members),*) - .fetch_optional(pool) - .await + .fetch_optional(executor) + .await } } } diff --git a/georm-macros/src/georm/traits/update.rs b/georm-macros/src/georm/traits/update.rs index 0ade52a..27bd544 100644 --- a/georm-macros/src/georm/traits/update.rs +++ b/georm-macros/src/georm/traits/update.rs @@ -28,14 +28,17 @@ pub fn generate_update_query(table_name: &str, fields: &[GeormField]) -> proc_ma where_clauses.join(" AND ") ); quote! { - async fn update(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { + async fn update<'e, E>(&self, mut executor: E) -> ::sqlx::Result + where + E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres> + { ::sqlx::query_as!( Self, #query, #(self.#update_idents),*, #(self.#id_idents),* ) - .fetch_one(pool) + .fetch_one(executor) .await } } diff --git a/georm-macros/src/georm/traits/upsert.rs b/georm-macros/src/georm/traits/upsert.rs index 711e3e5..aaf2c5c 100644 --- a/georm-macros/src/georm/traits/upsert.rs +++ b/georm-macros/src/georm/traits/upsert.rs @@ -44,13 +44,16 @@ pub fn generate_upsert_query( let field_idents: Vec = fields.iter().map(|f| f.ident.clone()).collect(); quote! { - async fn create_or_update(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { + async fn create_or_update<'e, E>(&self, mut executor: E) -> ::sqlx::Result + where + E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres> + { ::sqlx::query_as!( Self, #upsert_string, #(self.#field_idents),* ) - .fetch_one(pool) + .fetch_one(executor) .await } } diff --git a/src/defaultable.rs b/src/defaultable.rs index ffacfb5..4b9bff9 100644 --- a/src/defaultable.rs +++ b/src/defaultable.rs @@ -1,3 +1,5 @@ +use sqlx::{Executor, Postgres}; + /// Trait for creating entities with database defaults and auto-generated values. /// /// This trait is automatically implemented on generated companion structs for entities @@ -269,10 +271,11 @@ pub trait Defaultable { /// }; /// let created = post_default.create(&pool).await?; /// ``` - fn create( + fn create<'e, E>( &self, - pool: &sqlx::PgPool, + executor: E, ) -> impl std::future::Future> + Send where - Self: Sized; + Self: Sized, + E: Executor<'e, Database = Postgres>; } diff --git a/src/georm.rs b/src/georm.rs index 0f441ce..923e4c0 100644 --- a/src/georm.rs +++ b/src/georm.rs @@ -1,3 +1,5 @@ +use sqlx::{Executor, Postgres}; + /// Core database operations trait for Georm entities. /// /// This trait is automatically implemented by the `#[derive(Georm)]` macro and provides @@ -118,11 +120,12 @@ pub trait Georm { /// # Errors /// Returns `sqlx::Error` for database connection issues, permission problems, /// or if the table doesn't exist. - fn find_all( - pool: &sqlx::PgPool, + fn find_all<'e, E>( + executor: E, ) -> impl ::std::future::Future>> + Send where - Self: Sized; + Self: Sized, + E: Executor<'e, Database = Postgres>; /// Find a single entity by its primary key. /// @@ -152,12 +155,13 @@ pub trait Georm { /// Returns `sqlx::Error` for database connection issues, type conversion errors, /// or query execution problems. Note that not finding a record is not an error /// - it returns `Ok(None)`. - fn find( - pool: &sqlx::PgPool, + fn find<'e, E>( + executor: E, id: &Id, ) -> impl std::future::Future>> + Send where - Self: Sized; + Self: Sized, + E: Executor<'e, Database = Postgres>; /// Insert this entity as a new record in the database. /// @@ -187,16 +191,17 @@ pub trait Georm { /// # Errors /// Returns `sqlx::Error` for: /// - Unique constraint violations - /// - Foreign key constraint violations + /// - Foreign key constraint violations /// - NOT NULL constraint violations /// - Database connection issues /// - Permission problems - fn create( + fn create<'e, E>( &self, - pool: &sqlx::PgPool, + executor: E, ) -> impl std::future::Future> + Send where - Self: Sized; + Self: Sized, + E: Executor<'e, Database = Postgres>; /// Update an existing entity in the database. /// @@ -229,12 +234,13 @@ pub trait Georm { /// - Constraint violations (unique, foreign key, etc.) /// - Database connection issues /// - Permission problems - fn update( + fn update<'e, E>( &self, - pool: &sqlx::PgPool, + executor: E, ) -> impl std::future::Future> + Send where - Self: Sized; + Self: Sized, + E: Executor<'e, Database = Postgres>; /// Insert or update this entity using PostgreSQL's upsert functionality. /// @@ -267,12 +273,13 @@ pub trait Georm { /// - Non-primary-key constraint violations /// - Database connection issues /// - Permission problems - fn create_or_update( + fn create_or_update<'e, E>( &self, - pool: &sqlx::PgPool, + executor: E, ) -> impl ::std::future::Future> where - Self: Sized; + Self: Sized, + E: Executor<'e, Database = Postgres>; /// Delete this entity from the database. /// @@ -303,10 +310,12 @@ pub trait Georm { /// - Foreign key constraint violations (referenced by other tables) /// - Database connection issues /// - Permission problems - fn delete( + fn delete<'e, E>( &self, - pool: &sqlx::PgPool, - ) -> impl std::future::Future> + Send; + executor: E, + ) -> impl std::future::Future> + Send + where + E: Executor<'e, Database = Postgres>; /// Delete an entity by its primary key without needing an entity instance. /// @@ -341,10 +350,12 @@ pub trait Georm { /// - Foreign key constraint violations (referenced by other tables) /// - Database connection issues /// - Permission problems - fn delete_by_id( - pool: &sqlx::PgPool, + fn delete_by_id<'e, E>( + executor: E, id: &Id, - ) -> impl std::future::Future> + Send; + ) -> impl std::future::Future> + Send + where + E: Executor<'e, Database = Postgres>; /// Get the primary key of this entity. /// @@ -362,7 +373,7 @@ pub trait Georm { /// let user = User { id: 42, username: "alice".into(), email: "alice@example.com".into() }; /// let id = user.get_id(); // Returns 42 /// - /// // Composite primary key + /// // Composite primary key /// let user_role = UserRole { user_id: 1, role_id: 2, assigned_at: now }; /// let id = user_role.get_id(); // Returns UserRoleId { user_id: 1, role_id: 2 } /// ``` diff --git a/src/lib.rs b/src/lib.rs index 2049b20..a018548 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -200,7 +200,9 @@ //! } //! //! impl Defaultable for ProductDefault { -//! async fn create(&self, pool: &sqlx::PgPool) -> sqlx::Result; +//! async fn create<'e, E>(&self, pool: E) -> sqlx::Result +//! where +//! E: sqlx::Executor<'e, Database = sqlx::Postgres>; //! } //! ``` //! diff --git a/tests/models.rs b/tests/models.rs index 1ab9814..2c78e0d 100644 --- a/tests/models.rs +++ b/tests/models.rs @@ -1,5 +1,5 @@ use georm::Georm; -use sqlx::types::BigDecimal; +use sqlx::{Postgres, types::BigDecimal}; #[derive(Debug, Georm, PartialEq, Eq, Default)] #[georm( @@ -123,9 +123,12 @@ pub struct Product { impl Product { #[allow(dead_code)] - pub async fn find_by_name(name: String, pool: &sqlx::PgPool) -> ::sqlx::Result { + pub async fn find_by_name<'e, E>(name: String, executor: E) -> ::sqlx::Result + where + E: sqlx::Executor<'e, Database = Postgres>, + { ::sqlx::query_as!(Self, "SELECT * FROM products WHERE name = $1", name) - .fetch_one(pool) + .fetch_one(executor) .await } }