mirror of
https://github.com/Phundrak/georm.git
synced 2025-08-30 22:25:35 +00:00
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:
parent
3307aa679d
commit
49c7d86102
51
README.md
51
README.md
@ -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
|
||||||
|
|
||||||
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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? {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
57
src/georm.rs
57
src/georm.rs
@ -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 }
|
||||||
/// ```
|
/// ```
|
||||||
|
@ -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>;
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user