feat: enable transaction support via sqlx::Executor

This commit abstracts the database operations to use the generic
`sqlx::Executor` trait instead of a concrete `&sqlx::PgPool`.

This change allows all generated methods (find, create, update,
delete, and relationships) to be executed within a
`sqlx::Transaction`, in addition to a connection pool. This is a
crucial feature for ensuring atomic operations and data consistency.

The public-facing traits `Georm` and `Defaultable` have been updated
to require `sqlx::Executor`, and the documentation has been updated to
reflect this new capability.
This commit is contained in:
Lucien Cartier-Tilet 2025-08-09 12:19:06 +02:00
parent 3307aa679d
commit 49c7d86102
19 changed files with 230 additions and 112 deletions

View File

@ -38,6 +38,7 @@ Georm is a lightweight, opinionated Object-Relational Mapping (ORM) library buil
### Key Features ### Key Features
- **Type Safety**: Compile-time verified SQL queries using SQLx macros - **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 - **Zero Runtime Cost**: No reflection or runtime query building
- **Simple API**: Intuitive derive macros for common operations - **Simple API**: Intuitive derive macros for common operations
- **Relationship Support**: One-to-one, one-to-many, and many-to-many relationships - **Relationship Support**: One-to-one, one-to-many, and many-to-many relationships
@ -116,13 +117,16 @@ pub struct Post {
use sqlx::PgPool; use sqlx::PgPool;
async fn example(pool: &PgPool) -> sqlx::Result<()> { async fn example(pool: &PgPool) -> sqlx::Result<()> {
// Start a transaction
let mut tx = pool.begin().await?;
// Create an author // Create an author
let author = Author { let author = Author {
id: 0, // Will be auto-generated id: 0, // Will be auto-generated
name: "Jane Doe".to_string(), name: "Jane Doe".to_string(),
email: "jane@example.com".to_string(), email: "jane@example.com".to_string(),
}; };
let author = author.create(pool).await?; let author = author.create(&mut *tx).await?;
// Create a post // Create a post
let post = Post { let post = Post {
@ -133,9 +137,12 @@ async fn example(pool: &PgPool) -> sqlx::Result<()> {
author_id: author.id, author_id: author.id,
created_at: chrono::Utc::now(), 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?; let all_posts = Post::find_all(pool).await?;
// Get the post's author // Get the post's author
@ -521,22 +528,41 @@ pub struct Post {
### Core Operations ### Core Operations
All entities implementing `Georm<Id>` get these methods: All entities implementing `Georm<Id>` 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 ```rust
// Query operations // Query operations
Post::find_all(pool).await?; // Find all posts Post::find_all(executor).await?;
Post::find(pool, &post_id).await?; // Find by ID Post::find(executor, &post_id).await?;
// Mutation operations // Mutation operations
post.create(pool).await?; // Insert new record post.create(executor).await?;
post.update(pool).await?; // Update existing record post.update(executor).await?;
post.create_or_update(pool).await?; // Upsert operation post.create_or_update(executor).await?;
post.delete(pool).await?; // Delete this record post.delete(executor).await?;
Post::delete_by_id(pool, &post_id).await?; // Delete by ID Post::delete_by_id(executor, &post_id).await?;
// Utility // Utility
post.get_id(); // Get entity ID post.get_id();
``` ```
### Defaultable Operations ### Defaultable Operations
@ -624,7 +650,6 @@ cargo run help # For a list of all available actions
## Roadmap ## Roadmap
### High Priority ### 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 - **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 - **Multi-Database Support**: MySQL and SQLite support with feature flags

View File

@ -61,7 +61,8 @@ async fn create_comment(
pool: &sqlx::PgPool, pool: &sqlx::PgPool,
) -> Result { ) -> Result {
let prompt = "Who is creating the comment?"; 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 { let content = match text {
Some(text) => text, Some(text) => text,
None => inquire::Text::new("Content of the comment:") None => inquire::Text::new("Content of the comment:")
@ -73,29 +74,33 @@ async fn create_comment(
content, content,
id: None, id: None,
}; };
let comment = comment.create(pool).await?; let comment = comment.create(&mut *tx).await?;
tx.commit().await?;
println!("Successfuly created comment:\n{comment}"); println!("Successfuly created comment:\n{comment}");
Ok(()) Ok(())
} }
async fn remove_comment(id: Option<i32>, pool: &sqlx::PgPool) -> Result { async fn remove_comment(id: Option<i32>, pool: &sqlx::PgPool) -> Result {
let prompt = "Select the comment to remove:"; let prompt = "Select the comment to remove:";
let mut tx = pool.begin().await?;
let comment = match id { let comment = match id {
Some(id) => Comment::find(pool, &id) Some(id) => Comment::find(&mut *tx, &id)
.await .await
.map_err(UserInputError::DatabaseError)? .map_err(UserInputError::DatabaseError)?
.ok_or(UserInputError::CommentDoesNotExist)?, .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(()) Ok(())
} }
async fn remove_user_comment(username: Option<String>, pool: &sqlx::PgPool) -> Result { async fn remove_user_comment(username: Option<String>, pool: &sqlx::PgPool) -> Result {
let mut tx = pool.begin().await?;
let prompt = "Select user whose comment you want to delete:"; 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<String, Comment> = user let comments: HashMap<String, Comment> = user
.get_comments(pool) .get_comments(&mut *tx)
.await? .await?
.into_iter() .into_iter()
.map(|comment| (comment.content.clone(), comment)) .map(|comment| (comment.content.clone(), comment))
@ -105,7 +110,8 @@ async fn remove_user_comment(username: Option<String>, pool: &sqlx::PgPool) -> R
.prompt() .prompt()
.map_err(UserInputError::InquireError)?; .map_err(UserInputError::InquireError)?;
let comment: &Comment = comments.get(&selected_comment_content).unwrap(); let comment: &Comment = comments.get(&selected_comment_content).unwrap();
comment.delete(pool).await?; comment.delete(&mut *tx).await?;
tx.commit().await?;
Ok(()) Ok(())
} }

View File

@ -53,16 +53,17 @@ async fn follow_user(
followed: Option<String>, followed: Option<String>,
pool: &sqlx::PgPool, pool: &sqlx::PgPool,
) -> Result { ) -> Result {
let mut tx = pool.begin().await?;
let follower = User::get_user_by_username_or_select( let follower = User::get_user_by_username_or_select(
follower.as_deref(), follower.as_deref(),
"Select who will be following someone:", "Select who will be following someone:",
pool, &mut *tx,
) )
.await?; .await?;
let followed = User::get_user_by_username_or_select( let followed = User::get_user_by_username_or_select(
followed.as_deref(), followed.as_deref(),
"Select who will be followed:", "Select who will be followed:",
pool, &mut *tx,
) )
.await?; .await?;
let follow = FollowerDefault { let follow = FollowerDefault {
@ -70,17 +71,22 @@ async fn follow_user(
follower: follower.id, follower: follower.id,
followed: followed.id, followed: followed.id,
}; };
follow.create(pool).await?; follow.create(&mut *tx).await?;
tx.commit().await?;
println!("User {follower} now follows {followed}"); println!("User {follower} now follows {followed}");
Ok(()) Ok(())
} }
async fn unfollow_user(follower: Option<String>, pool: &sqlx::PgPool) -> Result { async fn unfollow_user(follower: Option<String>, pool: &sqlx::PgPool) -> Result {
let follower = let mut tx = pool.begin().await?;
User::get_user_by_username_or_select(follower.as_deref(), "Select who is following", pool) let follower = User::get_user_by_username_or_select(
.await?; follower.as_deref(),
"Select who is following",
&mut *tx,
)
.await?;
let followed_list: HashMap<String, User> = follower let followed_list: HashMap<String, User> = follower
.get_followed(pool) .get_followed(&mut *tx)
.await? .await?
.iter() .iter()
.map(|person| (person.username.clone(), person.clone())) .map(|person| (person.username.clone(), person.clone()))
@ -97,8 +103,9 @@ async fn unfollow_user(follower: Option<String>, pool: &sqlx::PgPool) -> Result
follower.id, follower.id,
followed.id followed.id
) )
.execute(pool) .execute(&mut *tx)
.await?; .await?;
tx.commit().await?;
println!("User {follower} unfollowed {followed}"); println!("User {follower} unfollowed {followed}");
Ok(()) Ok(())
} }

View File

@ -18,8 +18,11 @@ pub struct Comment {
} }
impl Comment { impl Comment {
pub async fn select_comment(prompt: &str, pool: &sqlx::PgPool) -> Result<Self> { pub async fn select_comment<'e, E>(prompt: &str, executor: E) -> Result<Self>
let comments: HashMap<String, Self> = Self::find_all(pool) where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
let comments: HashMap<String, Self> = Self::find_all(executor)
.await? .await?
.into_iter() .into_iter()
.map(|comment| (comment.content.clone(), comment)) .map(|comment| (comment.content.clone(), comment))

View File

@ -38,7 +38,10 @@ impl Profile {
self.bio.clone().unwrap_or_default() self.bio.clone().unwrap_or_default()
} }
pub async fn try_new(user_id: i32, pool: &sqlx::PgPool) -> Result<Self> { pub async fn try_new<'e, E>(user_id: i32, executor: E) -> Result<Self>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
let profile = ProfileDefault { let profile = ProfileDefault {
user_id, user_id,
id: None, id: None,
@ -46,20 +49,23 @@ impl Profile {
display_name: None, display_name: None,
}; };
profile profile
.create(pool) .create(executor)
.await .await
.map_err(UserInputError::DatabaseError) .map_err(UserInputError::DatabaseError)
} }
pub async fn update_interactive( pub async fn update_interactive<'e, E>(
&mut self, &mut self,
display_name: Option<String>, display_name: Option<String>,
bio: Option<String>, bio: Option<String>,
pool: &sqlx::PgPool, executor: E
) -> Result<Self> { ) -> Result<Self>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
self.display_name = display_name; self.display_name = display_name;
self.bio = bio; self.bio = bio;
self.update(pool) self.update(executor)
.await .await
.map_err(UserInputError::DatabaseError) .map_err(UserInputError::DatabaseError)
} }

View File

@ -50,8 +50,11 @@ impl From<&str> for UserDefault {
} }
impl User { impl User {
async fn select_user(prompt: &str, pool: &sqlx::PgPool) -> Result<Self> { async fn select_user<'e, E>(prompt: &str, executor: E) -> Result<Self>
let users: HashMap<String, Self> = Self::find_all(pool) where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
let users: HashMap<String, Self> = Self::find_all(executor)
.await? .await?
.into_iter() .into_iter()
.map(|user| (user.username.clone(), user)) .map(|user| (user.username.clone(), user))
@ -63,41 +66,50 @@ impl User {
Ok(user.clone()) 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<i32>, id: Option<i32>,
prompt: &str, prompt: &str,
pool: &sqlx::PgPool, executor: E
) -> Result<Self> { ) -> Result<Self>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
let user = match id { let user = match id {
Some(id) => Self::find(pool, &id) Some(id) => Self::find(executor, &id)
.await? .await?
.ok_or(UserInputError::UserDoesNotExist)?, .ok_or(UserInputError::UserDoesNotExist)?,
None => Self::select_user(prompt, pool).await?, None => Self::select_user(prompt, executor).await?,
}; };
Ok(user) 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>, username: Option<&str>,
prompt: &str, prompt: &str,
pool: &sqlx::PgPool, executor: E,
) -> Result<Self> { ) -> Result<Self>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
let user = match username { let user = match username {
Some(username) => Self::find_by_username(username, pool) Some(username) => Self::find_by_username(username, executor)
.await? .await?
.ok_or(UserInputError::UserDoesNotExist)?, .ok_or(UserInputError::UserDoesNotExist)?,
None => Self::select_user(prompt, pool).await?, None => Self::select_user(prompt, executor).await?,
}; };
Ok(user) Ok(user)
} }
pub async fn find_by_username(username: &str, pool: &sqlx::PgPool) -> Result<Option<Self>> { pub async fn find_by_username<'e, E>(username: &str, executor: E) -> Result<Option<Self>>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
sqlx::query_as!( sqlx::query_as!(
Self, Self,
"SELECT * FROM Users u WHERE u.username = $1", "SELECT * FROM Users u WHERE u.username = $1",
username username
) )
.fetch_optional(pool) .fetch_optional(executor)
.await .await
.map_err(UserInputError::DatabaseError) .map_err(UserInputError::DatabaseError)
} }
@ -116,7 +128,8 @@ impl User {
Ok(user) Ok(user)
} }
pub async fn update_profile(id: Option<i32>, pool: &sqlx::PgPool) -> Result<(User, Profile)> { pub async fn update_profile(id: Option<i32>, pool: &sqlx::PgPool) -> Result<(User, Profile)>
{
let prompt = "Select the user whose profile you want to update"; 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 user = Self::get_user_by_id_or_select(id, prompt, pool).await?;
let profile = match user.get_profile(pool).await? { let profile = match user.get_profile(pool).await? {

View File

@ -94,7 +94,10 @@ fn generate_defaultable_trait_impl(
quote! { quote! {
impl ::georm::Defaultable<#id_type, #struct_name> for #defaultable_struct_name { 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(); let mut dynamic_fields = Vec::new();
#(#field_checks)* #(#field_checks)*
@ -121,7 +124,7 @@ fn generate_defaultable_trait_impl(
// Then bind defaultable fields that have values // Then bind defaultable fields that have values
#(#bind_checks)* #(#bind_checks)*
query_builder.fetch_one(pool).await query_builder.fetch_one(executor).await
} }
} }
} }

View File

@ -71,8 +71,11 @@ WHERE local.{} = $1",
value.local.id value.local.id
); );
quote! { quote! {
pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Vec<#entity>> { pub async fn #function<'e, E>(&self, mut executor: E) -> ::sqlx::Result<Vec<#entity>>
::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(pool).await where
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
{
::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(executor).await
} }
} }
} }

View File

@ -172,8 +172,11 @@ impl From<&GeormField> for proc_macro2::TokenStream {
quote! { fetch_one } quote! { fetch_one }
}; };
quote! { quote! {
pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<#return_type> { pub async fn #function<'e, E>(&self, mut executor: E) -> ::sqlx::Result<#return_type>
::sqlx::query_as!(#entity, #query, self.#local_ident).#fetch(pool).await where
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
{
::sqlx::query_as!(#entity, #query, self.#local_ident).#fetch(executor).await
} }
} }
} }

View File

@ -45,8 +45,11 @@ impl From<&SimpleRelationship<OneToOne>> for proc_macro2::TokenStream {
let entity = &value.entity; let entity = &value.entity;
let function = value.make_function_name(); let function = value.make_function_name();
quote! { quote! {
pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Option<#entity>> { pub async fn #function<'e, E>(&self, mut executor: E) -> ::sqlx::Result<Option<#entity>>
::sqlx::query_as!(#entity, #query, self.get_id()).fetch_optional(pool).await 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<OneToMany>> for proc_macro2::TokenStream {
let entity = &value.entity; let entity = &value.entity;
let function = value.make_function_name(); let function = value.make_function_name();
quote! { quote! {
pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Vec<#entity>> { pub async fn #function<'e, E>(&self, mut executor: E) -> ::sqlx::Result<Vec<#entity>>
::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(pool).await where
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
{
::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(executor).await
} }
} }
} }

View File

@ -21,13 +21,16 @@ pub fn generate_create_query(table_name: &str, fields: &[GeormField]) -> proc_ma
placeholders.join(", ") placeholders.join(", ")
); );
quote! { quote! {
async fn create(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Self> { async fn create<'e, E>(&self, mut executor: E) -> ::sqlx::Result<Self>
where
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
{
::sqlx::query_as!( ::sqlx::query_as!(
Self, Self,
#query, #query,
#(self.#field_idents),* #(self.#field_idents),*
) )
.fetch_one(pool) .fetch_one(executor)
.await .await
} }
} }

View File

@ -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}"); let delete_string = format!("DELETE FROM {table} WHERE {where_clause}");
quote! { quote! {
async fn delete_by_id(pool: &::sqlx::PgPool, id: &#id_type) -> ::sqlx::Result<u64> { async fn delete<'e, E>(&self, mut executor: E) -> ::sqlx::Result<u64>
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<u64>
where
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
{
let rows_affected = ::sqlx::query!(#delete_string, #query_args) let rows_affected = ::sqlx::query!(#delete_string, #query_args)
.execute(pool) .execute(executor)
.await? .await?
.rows_affected(); .rows_affected();
Ok(rows_affected) Ok(rows_affected)
} }
async fn delete(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<u64> {
Self::delete_by_id(pool, &self.get_id()).await
}
} }
} }

View File

@ -4,8 +4,11 @@ use quote::quote;
pub fn generate_find_all_query(table: &str) -> proc_macro2::TokenStream { pub fn generate_find_all_query(table: &str) -> proc_macro2::TokenStream {
let find_string = format!("SELECT * FROM {table}"); let find_string = format!("SELECT * FROM {table}");
quote! { quote! {
async fn find_all(pool: &::sqlx::PgPool) -> ::sqlx::Result<Vec<Self>> { async fn find_all<'e, E>(mut executor: E) -> ::sqlx::Result<Vec<Self>>
::sqlx::query_as!(Self, #find_string).fetch_all(pool).await 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); let find_string = format!("SELECT * FROM {table} WHERE {} = $1", field_name);
quote! { quote! {
async fn find(pool: &::sqlx::PgPool, id: &#field_type) -> ::sqlx::Result<Option<Self>> { async fn find<'e, E>(mut executor: E, id: &#field_type) -> ::sqlx::Result<Option<Self>>
where
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
{
::sqlx::query_as!(Self, #find_string, id) ::sqlx::query_as!(Self, #find_string, id)
.fetch_optional(pool) .fetch_optional(executor)
.await .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(); fields.iter().map(|field| field.name.clone()).collect();
let find_string = format!("SELECT * FROM {table} WHERE {id_match_string}"); let find_string = format!("SELECT * FROM {table} WHERE {id_match_string}");
quote! { quote! {
async fn find(pool: &::sqlx::PgPool, id: &#field_type) -> ::sqlx::Result<Option<Self>> { async fn find<'e, E>(mut executor: E, id: &#field_type) -> ::sqlx::Result<Option<Self>>
where
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
{
::sqlx::query_as!(Self, #find_string, #(id.#id_members),*) ::sqlx::query_as!(Self, #find_string, #(id.#id_members),*)
.fetch_optional(pool) .fetch_optional(executor)
.await .await
} }
} }
} }

View File

@ -28,14 +28,17 @@ pub fn generate_update_query(table_name: &str, fields: &[GeormField]) -> proc_ma
where_clauses.join(" AND ") where_clauses.join(" AND ")
); );
quote! { quote! {
async fn update(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Self> { async fn update<'e, E>(&self, mut executor: E) -> ::sqlx::Result<Self>
where
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
{
::sqlx::query_as!( ::sqlx::query_as!(
Self, Self,
#query, #query,
#(self.#update_idents),*, #(self.#update_idents),*,
#(self.#id_idents),* #(self.#id_idents),*
) )
.fetch_one(pool) .fetch_one(executor)
.await .await
} }
} }

View File

@ -44,13 +44,16 @@ pub fn generate_upsert_query(
let field_idents: Vec<syn::Ident> = fields.iter().map(|f| f.ident.clone()).collect(); let field_idents: Vec<syn::Ident> = fields.iter().map(|f| f.ident.clone()).collect();
quote! { quote! {
async fn create_or_update(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Self> { async fn create_or_update<'e, E>(&self, mut executor: E) -> ::sqlx::Result<Self>
where
E: ::sqlx::Executor<'e, Database = ::sqlx::Postgres>
{
::sqlx::query_as!( ::sqlx::query_as!(
Self, Self,
#upsert_string, #upsert_string,
#(self.#field_idents),* #(self.#field_idents),*
) )
.fetch_one(pool) .fetch_one(executor)
.await .await
} }
} }

View File

@ -1,3 +1,5 @@
use sqlx::{Executor, Postgres};
/// Trait for creating entities with database defaults and auto-generated values. /// Trait for creating entities with database defaults and auto-generated values.
/// ///
/// This trait is automatically implemented on generated companion structs for entities /// This trait is automatically implemented on generated companion structs for entities
@ -269,10 +271,11 @@ pub trait Defaultable<Id, Entity> {
/// }; /// };
/// let created = post_default.create(&pool).await?; /// let created = post_default.create(&pool).await?;
/// ``` /// ```
fn create( fn create<'e, E>(
&self, &self,
pool: &sqlx::PgPool, executor: E,
) -> impl std::future::Future<Output = sqlx::Result<Entity>> + Send ) -> impl std::future::Future<Output = sqlx::Result<Entity>> + Send
where where
Self: Sized; Self: Sized,
E: Executor<'e, Database = Postgres>;
} }

View File

@ -1,3 +1,5 @@
use sqlx::{Executor, Postgres};
/// Core database operations trait for Georm entities. /// Core database operations trait for Georm entities.
/// ///
/// This trait is automatically implemented by the `#[derive(Georm)]` macro and provides /// This trait is automatically implemented by the `#[derive(Georm)]` macro and provides
@ -118,11 +120,12 @@ pub trait Georm<Id> {
/// # Errors /// # Errors
/// Returns `sqlx::Error` for database connection issues, permission problems, /// Returns `sqlx::Error` for database connection issues, permission problems,
/// or if the table doesn't exist. /// or if the table doesn't exist.
fn find_all( fn find_all<'e, E>(
pool: &sqlx::PgPool, executor: E,
) -> impl ::std::future::Future<Output = ::sqlx::Result<Vec<Self>>> + Send ) -> impl ::std::future::Future<Output = ::sqlx::Result<Vec<Self>>> + Send
where where
Self: Sized; Self: Sized,
E: Executor<'e, Database = Postgres>;
/// Find a single entity by its primary key. /// Find a single entity by its primary key.
/// ///
@ -152,12 +155,13 @@ pub trait Georm<Id> {
/// Returns `sqlx::Error` for database connection issues, type conversion errors, /// Returns `sqlx::Error` for database connection issues, type conversion errors,
/// or query execution problems. Note that not finding a record is not an error /// or query execution problems. Note that not finding a record is not an error
/// - it returns `Ok(None)`. /// - it returns `Ok(None)`.
fn find( fn find<'e, E>(
pool: &sqlx::PgPool, executor: E,
id: &Id, id: &Id,
) -> impl std::future::Future<Output = sqlx::Result<Option<Self>>> + Send ) -> impl std::future::Future<Output = sqlx::Result<Option<Self>>> + Send
where where
Self: Sized; Self: Sized,
E: Executor<'e, Database = Postgres>;
/// Insert this entity as a new record in the database. /// Insert this entity as a new record in the database.
/// ///
@ -187,16 +191,17 @@ pub trait Georm<Id> {
/// # Errors /// # Errors
/// Returns `sqlx::Error` for: /// Returns `sqlx::Error` for:
/// - Unique constraint violations /// - Unique constraint violations
/// - Foreign key constraint violations /// - Foreign key constraint violations
/// - NOT NULL constraint violations /// - NOT NULL constraint violations
/// - Database connection issues /// - Database connection issues
/// - Permission problems /// - Permission problems
fn create( fn create<'e, E>(
&self, &self,
pool: &sqlx::PgPool, executor: E,
) -> impl std::future::Future<Output = sqlx::Result<Self>> + Send ) -> impl std::future::Future<Output = sqlx::Result<Self>> + Send
where where
Self: Sized; Self: Sized,
E: Executor<'e, Database = Postgres>;
/// Update an existing entity in the database. /// Update an existing entity in the database.
/// ///
@ -229,12 +234,13 @@ pub trait Georm<Id> {
/// - Constraint violations (unique, foreign key, etc.) /// - Constraint violations (unique, foreign key, etc.)
/// - Database connection issues /// - Database connection issues
/// - Permission problems /// - Permission problems
fn update( fn update<'e, E>(
&self, &self,
pool: &sqlx::PgPool, executor: E,
) -> impl std::future::Future<Output = sqlx::Result<Self>> + Send ) -> impl std::future::Future<Output = sqlx::Result<Self>> + Send
where where
Self: Sized; Self: Sized,
E: Executor<'e, Database = Postgres>;
/// Insert or update this entity using PostgreSQL's upsert functionality. /// Insert or update this entity using PostgreSQL's upsert functionality.
/// ///
@ -267,12 +273,13 @@ pub trait Georm<Id> {
/// - Non-primary-key constraint violations /// - Non-primary-key constraint violations
/// - Database connection issues /// - Database connection issues
/// - Permission problems /// - Permission problems
fn create_or_update( fn create_or_update<'e, E>(
&self, &self,
pool: &sqlx::PgPool, executor: E,
) -> impl ::std::future::Future<Output = sqlx::Result<Self>> ) -> impl ::std::future::Future<Output = sqlx::Result<Self>>
where where
Self: Sized; Self: Sized,
E: Executor<'e, Database = Postgres>;
/// Delete this entity from the database. /// Delete this entity from the database.
/// ///
@ -303,10 +310,12 @@ pub trait Georm<Id> {
/// - Foreign key constraint violations (referenced by other tables) /// - Foreign key constraint violations (referenced by other tables)
/// - Database connection issues /// - Database connection issues
/// - Permission problems /// - Permission problems
fn delete( fn delete<'e, E>(
&self, &self,
pool: &sqlx::PgPool, executor: E,
) -> impl std::future::Future<Output = sqlx::Result<u64>> + Send; ) -> impl std::future::Future<Output = sqlx::Result<u64>> + Send
where
E: Executor<'e, Database = Postgres>;
/// Delete an entity by its primary key without needing an entity instance. /// Delete an entity by its primary key without needing an entity instance.
/// ///
@ -341,10 +350,12 @@ pub trait Georm<Id> {
/// - Foreign key constraint violations (referenced by other tables) /// - Foreign key constraint violations (referenced by other tables)
/// - Database connection issues /// - Database connection issues
/// - Permission problems /// - Permission problems
fn delete_by_id( fn delete_by_id<'e, E>(
pool: &sqlx::PgPool, executor: E,
id: &Id, id: &Id,
) -> impl std::future::Future<Output = sqlx::Result<u64>> + Send; ) -> impl std::future::Future<Output = sqlx::Result<u64>> + Send
where
E: Executor<'e, Database = Postgres>;
/// Get the primary key of this entity. /// Get the primary key of this entity.
/// ///
@ -362,7 +373,7 @@ pub trait Georm<Id> {
/// let user = User { id: 42, username: "alice".into(), email: "alice@example.com".into() }; /// let user = User { id: 42, username: "alice".into(), email: "alice@example.com".into() };
/// let id = user.get_id(); // Returns 42 /// 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 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 } /// let id = user_role.get_id(); // Returns UserRoleId { user_id: 1, role_id: 2 }
/// ``` /// ```

View File

@ -200,7 +200,9 @@
//! } //! }
//! //!
//! impl Defaultable<i32, Product> for ProductDefault { //! impl Defaultable<i32, Product> for ProductDefault {
//! async fn create(&self, pool: &sqlx::PgPool) -> sqlx::Result<Product>; //! async fn create<'e, E>(&self, pool: E) -> sqlx::Result<Post>
//! where
//! E: sqlx::Executor<'e, Database = sqlx::Postgres>;
//! } //! }
//! ``` //! ```
//! //!

View File

@ -1,5 +1,5 @@
use georm::Georm; use georm::Georm;
use sqlx::types::BigDecimal; use sqlx::{Postgres, types::BigDecimal};
#[derive(Debug, Georm, PartialEq, Eq, Default)] #[derive(Debug, Georm, PartialEq, Eq, Default)]
#[georm( #[georm(
@ -123,9 +123,12 @@ pub struct Product {
impl Product { impl Product {
#[allow(dead_code)] #[allow(dead_code)]
pub async fn find_by_name(name: String, pool: &sqlx::PgPool) -> ::sqlx::Result<Self> { pub async fn find_by_name<'e, E>(name: String, executor: E) -> ::sqlx::Result<Self>
where
E: sqlx::Executor<'e, Database = Postgres>,
{
::sqlx::query_as!(Self, "SELECT * FROM products WHERE name = $1", name) ::sqlx::query_as!(Self, "SELECT * FROM products WHERE name = $1", name)
.fetch_one(pool) .fetch_one(executor)
.await .await
} }
} }