sqlx-template 0.2.1

Template query library for Rust SQLx
Documentation
use proc_macro2::TokenStream;
use quote::quote;
use syn::{
    parse_macro_input, token::Eq, Attribute, Data, DeriveInput, Field, Fields, Ident, Lit, LitStr, Meta, MetaList, MetaNameValue, NestedMeta, Path, Token
};

use crate::sqlx_template::{get_database_from_ast, Database};

use super::{get_table_name, Scope};

pub fn derive_insert(ast: &DeriveInput, for_path: Option<&Path>, scope: Scope, db: Option<Database>) -> syn::Result<TokenStream> {
    let struct_name = &ast.ident;
    let struct_name = match for_path {
        Some(path) => quote! {#path},
        None => quote! {#struct_name},
    };
    let mut fields = vec![];
    let debug_slow = super::get_debug_slow_from_table_scope(&ast);
    if let syn::Data::Struct(syn::DataStruct {
        fields: syn::Fields::Named(syn::FieldsNamed { ref named, .. }),
        ..
    }) = ast.data
    {
        named.iter().for_each(|f| {
            if !has_auto_attribute(f) {
                if let Some(ident) = f.ident.as_ref() {
                    fields.push(ident);
                }
            };
        })
    } else {
        panic!("InsertTemplate macro only works with structs with named fields");
    };

    if fields.is_empty() {
        panic!("Must have at least one field with no auto attribute");
    }

    let table_name = get_table_name(&ast);
    let db = db.or_else(|| Some(get_database_from_ast(&ast))).expect("Missing db config");
    let sql_fields = fields
        .iter()
        .map(|f| super::check_column_name(f.to_string(), db))
        .collect::<Vec<_>>()
        .join(", ");
    let sql_placeholders = match db {
        Database::Postgres => (1..=fields.len())
            .map(|i| format!("${}", i))
            .collect::<Vec<_>>()
            .join(", "),
        Database::Sqlite | Database::Mysql | Database::Any => (1..=fields.len())
            .map(|_| "?".to_string())
            .collect::<Vec<_>>()
            .join(", "),
    };
    let sql = format!(
        "INSERT INTO {table_name}({sql_fields}) VALUES ({sql_placeholders})"
    );
    super::check_valid_single_sql(&sql, db);
    let sql_return = format!(
        "INSERT INTO {table_name}({sql_fields}) VALUES ({sql_placeholders}) RETURNING *"
    );
    
    let binds = fields.iter().map(|field| {
        quote! {
            .bind(&re.#field)
        }
    });

    let binds_return = binds.clone();

    let database = super::get_database_type(db);
    let (dbg_before, dbg_after) = super::gen_debug_code(debug_slow);
    let insert = quote! {
        pub async fn insert<'c, E: sqlx::Executor<'c, Database = #database>>(re: &#struct_name, conn: E) -> Result<u64, sqlx::Error> {
            let sql = #sql;
            #dbg_before
            let query = sqlx::query(sql)
                #(#binds)*
                .execute(conn)
                .await;
            #dbg_after
            Ok(query?.rows_affected())
        }
    };
    let insert = super::gen_with_doc(insert);

    
    let insert_returning = if matches!(db, Database::Postgres) {
        super::check_valid_single_sql(&sql_return, db);
        let insert_returning = quote! {
            pub async fn insert_return<'c, E: sqlx::Executor<'c, Database = #database>>(re: &#struct_name, conn: E) -> Result<#struct_name, sqlx::Error> {
                let sql = #sql_return;
                #dbg_before
                let res = sqlx::query_as::<_, #struct_name>(sql)
                    #(#binds_return)*
                    .fetch_one(conn)
                    .await;
                #dbg_after
                Ok(res?)
            }
        };
        super::gen_with_doc(insert_returning)
    } else {
        quote! {}
    };


    let gen = match scope {
        Scope::Struct => quote! {
            impl #struct_name {
                #insert

                #insert_returning
            }
        },
        Scope::Mod => quote! {
            #insert

            #insert_returning
        },
        super::Scope::NewMod => {
            let new_mod = super::create_ident(&table_name);
            quote! {
                pub mod #new_mod {
                    #insert

                    #insert_returning
                }
            }
        },
    };

    Ok(gen.into())
}

fn has_auto_attribute(field: &Field) -> bool {
    field.attrs.iter().any(|attr| attr.path.is_ident("auto"))
}