diff --git a/georm-macros/src/georm/mod.rs b/georm-macros/src/georm/mod.rs index 817d953..63c82b5 100644 --- a/georm-macros/src/georm/mod.rs +++ b/georm-macros/src/georm/mod.rs @@ -1,11 +1,12 @@ -use ir::GeormField; use quote::quote; mod composite_keys; mod defaultable_struct; mod ir; +pub(crate) use ir::GeormField; mod relationships; -mod trait_implementation; +mod traits; +pub(crate) use composite_keys::IdType; fn extract_georm_field_attrs(ast: &mut syn::DeriveInput) -> deluxe::Result> { let syn::Data::Struct(s) = &mut ast.data else { @@ -50,8 +51,7 @@ pub fn georm_derive_macro2( let relationships = relationships::derive_relationships(&ast, &struct_attrs, &fields, &identifier); - let trait_impl = - trait_implementation::derive_trait(&ast, &struct_attrs.table, &fields, &identifier); + let trait_impl = traits::derive_trait(&ast, &struct_attrs.table, &fields, &identifier); let code = quote! { #id_struct diff --git a/georm-macros/src/georm/trait_implementation.rs b/georm-macros/src/georm/trait_implementation.rs deleted file mode 100644 index 9b9e277..0000000 --- a/georm-macros/src/georm/trait_implementation.rs +++ /dev/null @@ -1,268 +0,0 @@ -use super::composite_keys::IdType; -use super::ir::GeormField; -use quote::quote; - -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 - } - } -} - -fn generate_find_query(table: &str, id: &IdType) -> proc_macro2::TokenStream { - match id { - IdType::Simple { - field_name, - field_type, - } => { - let find_string = format!("SELECT * FROM {table} WHERE {} = $1", field_name); - quote! { - async fn find(pool: &::sqlx::PgPool, id: &#field_type) -> ::sqlx::Result> { - ::sqlx::query_as!(Self, #find_string, id) - .fetch_optional(pool) - .await - } - } - } - IdType::Composite { fields, field_type } => { - let id_match_string = fields - .iter() - .enumerate() - .map(|(i, field)| format!("{} = ${}", field.name, i + 1)) - .collect::>() - .join(" AND "); - let id_members: Vec = - 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> { - ::sqlx::query_as!(Self, #find_string, #(id.#id_members),*) - .fetch_optional(pool) - .await - } - } - } - } -} - -fn generate_create_query(table: &str, fields: &[GeormField]) -> proc_macro2::TokenStream { - let inputs: Vec = (1..=fields.len()).map(|num| format!("${num}")).collect(); - let create_string = format!( - "INSERT INTO {table} ({}) VALUES ({}) RETURNING *", - fields - .iter() - .map(|f| f.ident.to_string()) - .collect::>() - .join(", "), - inputs.join(", ") - ); - let field_idents: Vec = fields.iter().map(|f| f.ident.clone()).collect(); - quote! { - async fn create(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { - ::sqlx::query_as!( - Self, - #create_string, - #(self.#field_idents),* - ) - .fetch_one(pool) - .await - } - } -} - -fn generate_update_query( - table: &str, - fields: &[GeormField], - id: &IdType, -) -> proc_macro2::TokenStream { - let non_id_fields: Vec = fields - .iter() - .filter_map(|f| if f.id { None } else { Some(f.ident.clone()) }) - .collect(); - let update_columns = non_id_fields - .iter() - .enumerate() - .map(|(i, field)| format!("{} = ${}", field, i + 1)) - .collect::>() - .join(", "); - let mut all_fields = non_id_fields.clone(); - let where_clause = match id { - IdType::Simple { field_name, .. } => { - let where_clause = format!("{} = ${}", field_name, non_id_fields.len() + 1); - all_fields.push(field_name.clone()); - where_clause - } - IdType::Composite { fields, .. } => fields - .iter() - .enumerate() - .map(|(i, field)| { - let where_clause = format!("{} = ${}", field.name, non_id_fields.len() + i + 1); - all_fields.push(field.name.clone()); - where_clause - }) - .collect::>() - .join(" AND "), - }; - let update_string = - format!("UPDATE {table} SET {update_columns} WHERE {where_clause} RETURNING *"); - quote! { - async fn update(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { - ::sqlx::query_as!( - Self, #update_string, #(self.#all_fields),* - ) - .fetch_one(pool) - .await - } - } -} - -fn generate_delete_query(table: &str, id: &IdType) -> proc_macro2::TokenStream { - let where_clause = match id { - IdType::Simple { field_name, .. } => format!("{} = $1", field_name), - IdType::Composite { fields, .. } => fields - .iter() - .enumerate() - .map(|(i, field)| format!("{} = ${}", field.name, i + 1)) - .collect::>() - .join(" AND "), - }; - let query_args = match id { - IdType::Simple { .. } => quote! { id }, - IdType::Composite { fields, .. } => { - let fields: Vec = fields.iter().map(|f| f.name.clone()).collect(); - quote! { #(id.#fields), * } - } - }; - let id_type = match id { - IdType::Simple { field_type, .. } => quote! { #field_type }, - IdType::Composite { field_type, .. } => quote! { #field_type }, - }; - let delete_string = format!("DELETE FROM {table} WHERE {where_clause}"); - quote! { - async fn delete_by_id(pool: &::sqlx::PgPool, id: &#id_type) -> ::sqlx::Result { - let rows_affected = ::sqlx::query!(#delete_string, #query_args) - .execute(pool) - .await? - .rows_affected(); - Ok(rows_affected) - } - - async fn delete(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { - Self::delete_by_id(pool, &self.get_id()).await - } - } -} - -fn generate_upsert_query( - table: &str, - fields: &[GeormField], - id: &IdType, -) -> proc_macro2::TokenStream { - let inputs: Vec = (1..=fields.len()).map(|num| format!("${num}")).collect(); - let columns = fields - .iter() - .map(|f| f.ident.to_string()) - .collect::>() - .join(", "); - - let primary_key: proc_macro2::TokenStream = match id { - IdType::Simple { field_name, .. } => quote! {#field_name}, - IdType::Composite { fields, .. } => { - let field_names: Vec = fields.iter().map(|f| f.name.clone()).collect(); - quote! { - #(#field_names),* - } - } - }; - - // For ON CONFLICT DO UPDATE, exclude the ID field from updates - let update_assignments = fields - .iter() - .filter(|f| !f.id) - .map(|f| format!("{} = EXCLUDED.{}", f.ident, f.ident)) - .collect::>() - .join(", "); - - let upsert_string = format!( - "INSERT INTO {table} ({columns}) VALUES ({}) ON CONFLICT ({}) DO UPDATE SET {update_assignments} RETURNING *", - inputs.join(", "), - primary_key - ); - - let field_idents: Vec = fields.iter().map(|f| f.ident.clone()).collect(); - - quote! { - async fn create_or_update(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { - ::sqlx::query_as!( - Self, - #upsert_string, - #(self.#field_idents),* - ) - .fetch_one(pool) - .await - } - } -} - -fn generate_get_id(id: &IdType) -> proc_macro2::TokenStream { - match id { - IdType::Simple { - field_name, - field_type, - } => { - quote! { - fn get_id(&self) -> #field_type { - self.#field_name.clone() - } - } - } - IdType::Composite { fields, field_type } => { - let field_names: Vec = fields.iter().map(|f| f.name.clone()).collect(); - quote! { - fn get_id(&self) -> #field_type { - #field_type { - #(#field_names: self.#field_names),* - } - } - } - } - } -} - -pub fn derive_trait( - ast: &syn::DeriveInput, - table: &str, - fields: &[GeormField], - id: &IdType, -) -> proc_macro2::TokenStream { - let ty = match id { - IdType::Simple { field_type, .. } => quote! {#field_type}, - IdType::Composite { field_type, .. } => quote! {#field_type}, - }; - - // define impl variables - let ident = &ast.ident; - let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); - - // generate - let get_all = generate_find_all_query(table); - let get_id = generate_get_id(id); - let find_query = generate_find_query(table, id); - let create_query = generate_create_query(table, fields); - let update_query = generate_update_query(table, fields, id); - let upsert_query = generate_upsert_query(table, fields, id); - let delete_query = generate_delete_query(table, id); - quote! { - impl #impl_generics Georm<#ty> for #ident #type_generics #where_clause { - #get_all - #get_id - #find_query - #create_query - #update_query - #upsert_query - #delete_query - } - } -} diff --git a/georm-macros/src/georm/traits/create.rs b/georm-macros/src/georm/traits/create.rs new file mode 100644 index 0000000..774d9e8 --- /dev/null +++ b/georm-macros/src/georm/traits/create.rs @@ -0,0 +1,27 @@ +use crate::georm::GeormField; +use quote::quote; + +pub fn generate_create_query(table: &str, fields: &[GeormField]) -> proc_macro2::TokenStream { + let inputs: Vec = (1..=fields.len()).map(|num| format!("${num}")).collect(); + let create_string = format!( + "INSERT INTO {table} ({}) VALUES ({}) RETURNING *", + fields + .iter() + .map(|f| f.ident.to_string()) + .collect::>() + .join(", "), + inputs.join(", ") + ); + let field_idents: Vec = fields.iter().map(|f| f.ident.clone()).collect(); + quote! { + async fn create(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { + ::sqlx::query_as!( + Self, + #create_string, + #(self.#field_idents),* + ) + .fetch_one(pool) + .await + } + } +} diff --git a/georm-macros/src/georm/traits/delete.rs b/georm-macros/src/georm/traits/delete.rs new file mode 100644 index 0000000..7cc4ee7 --- /dev/null +++ b/georm-macros/src/georm/traits/delete.rs @@ -0,0 +1,39 @@ +use crate::georm::IdType; +use quote::quote; + +pub fn generate_delete_query(table: &str, id: &IdType) -> proc_macro2::TokenStream { + let where_clause = match id { + IdType::Simple { field_name, .. } => format!("{} = $1", field_name), + IdType::Composite { fields, .. } => fields + .iter() + .enumerate() + .map(|(i, field)| format!("{} = ${}", field.name, i + 1)) + .collect::>() + .join(" AND "), + }; + let query_args = match id { + IdType::Simple { .. } => quote! { id }, + IdType::Composite { fields, .. } => { + let fields: Vec = fields.iter().map(|f| f.name.clone()).collect(); + quote! { #(id.#fields), * } + } + }; + let id_type = match id { + IdType::Simple { field_type, .. } => quote! { #field_type }, + IdType::Composite { field_type, .. } => quote! { #field_type }, + }; + let delete_string = format!("DELETE FROM {table} WHERE {where_clause}"); + quote! { + async fn delete_by_id(pool: &::sqlx::PgPool, id: &#id_type) -> ::sqlx::Result { + let rows_affected = ::sqlx::query!(#delete_string, #query_args) + .execute(pool) + .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 new file mode 100644 index 0000000..c47e9f8 --- /dev/null +++ b/georm-macros/src/georm/traits/find.rs @@ -0,0 +1,47 @@ +use crate::georm::IdType; +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 + } + } +} + +pub fn generate_find_query(table: &str, id: &IdType) -> proc_macro2::TokenStream { + match id { + IdType::Simple { + field_name, + field_type, + } => { + let find_string = format!("SELECT * FROM {table} WHERE {} = $1", field_name); + quote! { + async fn find(pool: &::sqlx::PgPool, id: &#field_type) -> ::sqlx::Result> { + ::sqlx::query_as!(Self, #find_string, id) + .fetch_optional(pool) + .await + } + } + } + IdType::Composite { fields, field_type } => { + let id_match_string = fields + .iter() + .enumerate() + .map(|(i, field)| format!("{} = ${}", field.name, i + 1)) + .collect::>() + .join(" AND "); + let id_members: Vec = + 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> { + ::sqlx::query_as!(Self, #find_string, #(id.#id_members),*) + .fetch_optional(pool) + .await + } + } + } + } +} diff --git a/georm-macros/src/georm/traits/mod.rs b/georm-macros/src/georm/traits/mod.rs new file mode 100644 index 0000000..493bab4 --- /dev/null +++ b/georm-macros/src/georm/traits/mod.rs @@ -0,0 +1,70 @@ +use super::composite_keys::IdType; +use super::ir::GeormField; +use quote::quote; + +mod create; +mod delete; +mod find; +mod update; +mod upsert; + +fn generate_get_id(id: &IdType) -> proc_macro2::TokenStream { + match id { + IdType::Simple { + field_name, + field_type, + } => { + quote! { + fn get_id(&self) -> #field_type { + self.#field_name.clone() + } + } + } + IdType::Composite { fields, field_type } => { + let field_names: Vec = fields.iter().map(|f| f.name.clone()).collect(); + quote! { + fn get_id(&self) -> #field_type { + #field_type { + #(#field_names: self.#field_names),* + } + } + } + } + } +} + +pub fn derive_trait( + ast: &syn::DeriveInput, + table: &str, + fields: &[GeormField], + id: &IdType, +) -> proc_macro2::TokenStream { + let ty = match id { + IdType::Simple { field_type, .. } => quote! {#field_type}, + IdType::Composite { field_type, .. } => quote! {#field_type}, + }; + + // define impl variables + let ident = &ast.ident; + let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); + + // generate + let get_id = generate_get_id(id); + let get_all = find::generate_find_all_query(table); + let find_query = find::generate_find_query(table, id); + let create_query = create::generate_create_query(table, fields); + let update_query = update::generate_update_query(table, fields, id); + let upsert_query = upsert::generate_upsert_query(table, fields, id); + let delete_query = delete::generate_delete_query(table, id); + quote! { + impl #impl_generics Georm<#ty> for #ident #type_generics #where_clause { + #get_all + #get_id + #find_query + #create_query + #update_query + #upsert_query + #delete_query + } + } +} diff --git a/georm-macros/src/georm/traits/update.rs b/georm-macros/src/georm/traits/update.rs new file mode 100644 index 0000000..0059139 --- /dev/null +++ b/georm-macros/src/georm/traits/update.rs @@ -0,0 +1,48 @@ +use crate::georm::{GeormField, IdType}; +use quote::quote; + +pub fn generate_update_query( + table: &str, + fields: &[GeormField], + id: &IdType, +) -> proc_macro2::TokenStream { + let non_id_fields: Vec = fields + .iter() + .filter_map(|f| if f.id { None } else { Some(f.ident.clone()) }) + .collect(); + let update_columns = non_id_fields + .iter() + .enumerate() + .map(|(i, field)| format!("{} = ${}", field, i + 1)) + .collect::>() + .join(", "); + let mut all_fields = non_id_fields.clone(); + let where_clause = match id { + IdType::Simple { field_name, .. } => { + let where_clause = format!("{} = ${}", field_name, non_id_fields.len() + 1); + all_fields.push(field_name.clone()); + where_clause + } + IdType::Composite { fields, .. } => fields + .iter() + .enumerate() + .map(|(i, field)| { + let where_clause = format!("{} = ${}", field.name, non_id_fields.len() + i + 1); + all_fields.push(field.name.clone()); + where_clause + }) + .collect::>() + .join(" AND "), + }; + let update_string = + format!("UPDATE {table} SET {update_columns} WHERE {where_clause} RETURNING *"); + quote! { + async fn update(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { + ::sqlx::query_as!( + Self, #update_string, #(self.#all_fields),* + ) + .fetch_one(pool) + .await + } + } +} diff --git a/georm-macros/src/georm/traits/upsert.rs b/georm-macros/src/georm/traits/upsert.rs new file mode 100644 index 0000000..a44b288 --- /dev/null +++ b/georm-macros/src/georm/traits/upsert.rs @@ -0,0 +1,53 @@ +use crate::georm::{GeormField, IdType}; +use quote::quote; + +pub fn generate_upsert_query( + table: &str, + fields: &[GeormField], + id: &IdType, +) -> proc_macro2::TokenStream { + let inputs: Vec = (1..=fields.len()).map(|num| format!("${num}")).collect(); + let columns = fields + .iter() + .map(|f| f.ident.to_string()) + .collect::>() + .join(", "); + + let primary_key: proc_macro2::TokenStream = match id { + IdType::Simple { field_name, .. } => quote! {#field_name}, + IdType::Composite { fields, .. } => { + let field_names: Vec = fields.iter().map(|f| f.name.clone()).collect(); + quote! { + #(#field_names),* + } + } + }; + + // For ON CONFLICT DO UPDATE, exclude the ID field from updates + let update_assignments = fields + .iter() + .filter(|f| !f.id) + .map(|f| format!("{} = EXCLUDED.{}", f.ident, f.ident)) + .collect::>() + .join(", "); + + let upsert_string = format!( + "INSERT INTO {table} ({columns}) VALUES ({}) ON CONFLICT ({}) DO UPDATE SET {update_assignments} RETURNING *", + inputs.join(", "), + primary_key + ); + + let field_idents: Vec = fields.iter().map(|f| f.ident.clone()).collect(); + + quote! { + async fn create_or_update(&self, pool: &::sqlx::PgPool) -> ::sqlx::Result { + ::sqlx::query_as!( + Self, + #upsert_string, + #(self.#field_idents),* + ) + .fetch_one(pool) + .await + } + } +}