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:
2025-08-09 12:19:06 +02:00
parent 3307aa679d
commit 49c7d86102
19 changed files with 230 additions and 112 deletions

View File

@@ -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
}
}
}

View File

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

View File

@@ -45,8 +45,11 @@ impl From<&SimpleRelationship<OneToOne>> 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<Option<#entity>> {
::sqlx::query_as!(#entity, #query, self.get_id()).fetch_optional(pool).await
pub async fn #function<'e, E>(&self, mut executor: E) -> ::sqlx::Result<Option<#entity>>
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 function = value.make_function_name();
quote! {
pub async fn #function(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result<Vec<#entity>> {
::sqlx::query_as!(#entity, #query, self.get_id()).fetch_all(pool).await
pub async fn #function<'e, E>(&self, mut executor: E) -> ::sqlx::Result<Vec<#entity>>
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(", ")
);
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!(
Self,
#query,
#(self.#field_idents),*
)
.fetch_one(pool)
.fetch_one(executor)
.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}");
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)
.execute(pool)
.execute(executor)
.await?
.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 {
let find_string = format!("SELECT * FROM {table}");
quote! {
async fn find_all(pool: &::sqlx::PgPool) -> ::sqlx::Result<Vec<Self>> {
::sqlx::query_as!(Self, #find_string).fetch_all(pool).await
async fn find_all<'e, E>(mut executor: E) -> ::sqlx::Result<Vec<Self>>
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<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)
.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<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),*)
.fetch_optional(pool)
.await
.fetch_optional(executor)
.await
}
}
}

View File

@@ -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<Self> {
async fn update<'e, E>(&self, mut executor: E) -> ::sqlx::Result<Self>
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
}
}

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();
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!(
Self,
#upsert_string,
#(self.#field_idents),*
)
.fetch_one(pool)
.fetch_one(executor)
.await
}
}