Compare commits

..

No commits in common. "f8b1a0aeabf6336b39a3b3b9a89a4ded276be3d1" and "a969acca6ad0aa8849b5bd41c491413f8532152d" have entirely different histories.

3 changed files with 28 additions and 91 deletions

View File

@ -1,10 +1,10 @@
use std::collections::HashMap;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::{quote, ToTokens}; use quote::{quote, ToTokens};
use syn::{parse_macro_input, Data, DeriveInput, Fields}; use syn::{parse_macro_input, Data, DeriveInput, Fields};
// TODO: wrap functions in a trait? would probably use the other crate // TODO: wrap functions in a trait
// TODO: doc comments
#[proc_macro_derive(Table)] #[proc_macro_derive(Table)]
pub fn derive_table(input: TokenStream) -> TokenStream { pub fn derive_table(input: TokenStream) -> TokenStream {
@ -29,8 +29,8 @@ pub fn derive_table(input: TokenStream) -> TokenStream {
let mut field_getters = Vec::new(); let mut field_getters = Vec::new();
let mut field_accessors = Vec::new(); let mut field_accessors = Vec::new();
let mut to_sql_trait_bounds = HashMap::new(); let mut to_sql_trait_bounds = Vec::new();
let mut from_sql_trait_bounds = HashMap::new(); let mut from_sql_trait_bounds = Vec::new();
for field in fields.named.iter() { for field in fields.named.iter() {
let field_name = field.ident.as_ref().unwrap(); let field_name = field.ident.as_ref().unwrap();
@ -40,8 +40,8 @@ pub fn derive_table(input: TokenStream) -> TokenStream {
field_getters.push(quote!(#field_name: row.get(stringify!(#field_name))?)); field_getters.push(quote!(#field_name: row.get(stringify!(#field_name))?));
field_accessors.push(quote!(self.#field_name)); field_accessors.push(quote!(self.#field_name));
to_sql_trait_bounds.insert(stringify!(#field_type), quote!(#field_type: rusqlite::types::ToSql)); to_sql_trait_bounds.push(quote!(#field_type: rusqlite::types::ToSql));
from_sql_trait_bounds.insert(stringify!(#field_type), quote!(#field_type: rusqlite::types::FromSql)); from_sql_trait_bounds.push(quote!(#field_type: rusqlite::types::FromSql));
if field_name == "id" { if field_name == "id" {
if let syn::Type::Path(type_path) = field_type { if let syn::Type::Path(type_path) = field_type {
@ -59,9 +59,6 @@ pub fn derive_table(input: TokenStream) -> TokenStream {
} }
} }
let to_sql_trait_bounds = to_sql_trait_bounds.values().collect::<Vec<_>>();
let from_sql_trait_bounds = from_sql_trait_bounds.values().collect::<Vec<_>>();
if !field_names.iter().map(|id| id.to_string()).any(|id| &id == "id") { if !field_names.iter().map(|id| id.to_string()).any(|id| &id == "id") {
panic!("Structs annotated with `Table` require a primary key field `id: Option<i64>`."); panic!("Structs annotated with `Table` require a primary key field `id: Option<i64>`.");
} }
@ -74,8 +71,6 @@ pub fn derive_table(input: TokenStream) -> TokenStream {
); );
let create_table_fn = quote! { let create_table_fn = quote! {
/// Use a `Connection` to create a table named after the struct (`#struct_name`)
/// If the table already exists, this returns `Ok(())` and does nothing.
pub fn create_table(conn: &rusqlite::Connection) -> rusqlite::Result<()> pub fn create_table(conn: &rusqlite::Connection) -> rusqlite::Result<()>
where #(#to_sql_trait_bounds),* where #(#to_sql_trait_bounds),*
{ {
@ -93,13 +88,10 @@ pub fn derive_table(input: TokenStream) -> TokenStream {
); );
let insert_fn = quote! { let insert_fn = quote! {
/// Insert struct instance into the table, setting `self.id` to
/// `Some(last_insert_rowid())` if it was `None`.
pub fn insert(&mut self, conn: &rusqlite::Connection) -> rusqlite::Result<i64> pub fn insert(&mut self, conn: &rusqlite::Connection) -> rusqlite::Result<i64>
where #(#to_sql_trait_bounds),* where #(#to_sql_trait_bounds),*
{ {
conn.execute(#insert_sql, rusqlite::params![#(#field_accessors),*])?; conn.execute(#insert_sql, rusqlite::params![#(#field_accessors),*])?;
// TODO: test this with manually set id. also test that this can't update!!!
let id = conn.last_insert_rowid(); let id = conn.last_insert_rowid();
self.id = Some(id); self.id = Some(id);
Ok(id) Ok(id)
@ -107,10 +99,8 @@ pub fn derive_table(input: TokenStream) -> TokenStream {
}; };
let update_or_insert_fn = quote! { let upsert_fn = quote! {
/// Update a table row using the calling struct instance. pub fn upsert(&mut self, conn: &rusqlite::Connection) -> rusqlite::Result<i64>
/// If the row does not yet exist, it is inserted into the table.
pub fn update_or_insert(&mut self, conn: &rusqlite::Connection) -> rusqlite::Result<i64>
where #(#to_sql_trait_bounds),* where #(#to_sql_trait_bounds),*
{ {
match self.id { match self.id {
@ -134,17 +124,10 @@ pub fn derive_table(input: TokenStream) -> TokenStream {
); );
let update_fn = quote! { let update_fn = quote! {
/// Update a table row using the calling struct instance.
///
/// If the row does not yet exist, this fails.
/// A version that inserts a new row instead also exists. See `update_or_insert`.
///
/// Result contains `true` if a row was updated.
pub fn update(&self, conn: &rusqlite::Connection) -> rusqlite::Result<bool> pub fn update(&self, conn: &rusqlite::Connection) -> rusqlite::Result<bool>
where #(#to_sql_trait_bounds),* where #(#to_sql_trait_bounds),*
{ {
if self.id.is_none() { if self.id.is_none() {
// TODO: bad design, should probably fail instead
return Ok(false); return Ok(false);
} }
let updated_count = conn.execute(#update_sql, rusqlite::params![#(#field_accessors),*])?; let updated_count = conn.execute(#update_sql, rusqlite::params![#(#field_accessors),*])?;
@ -154,45 +137,23 @@ pub fn derive_table(input: TokenStream) -> TokenStream {
let sync_fn = quote! { let sync_fn = quote! {
/// Sync a struct instance with the database state.
/// This "updates" the structs fields using its database entry.
///
/// Result contains `false` if `self.id == None` or if no row with that `id` was found.
///
/// To update database entry using the structs fields, see `update`.
pub fn sync(&mut self, conn: &rusqlite::Connection) -> rusqlite::Result<bool> pub fn sync(&mut self, conn: &rusqlite::Connection) -> rusqlite::Result<bool>
where #(#from_sql_trait_bounds),* where #(#from_sql_trait_bounds),*
{ {
if self.id.is_none() { if self.id.is_none() {
return Ok(false); return Ok(false);
} }
match #struct_name::get_by_id(conn, self.id.unwrap()) { match #struct_name::get_by_id(conn, self.id.unwrap())? {
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false), Some(person) => *self = person,
Ok(person) => { _ => return Ok(false),
*self = person;
Ok(true)
},
Err(e) => Err(e),
}
}
}; };
Ok(true)
let from_sql_row_fn = quote! {
/// Convert a `rusqlite::Row` received through a query to an instance of the struct
pub fn from_sql_row(row: &rusqlite::Row) -> rusqlite::Result<Self>
where
Self: Sized,
#(#from_sql_trait_bounds),*
{
Ok(Self { #(#field_getters),* })
} }
}; };
let get_by_id_fn = quote! { let get_by_id_fn = quote! {
/// Get a person from the table using their `id` (corresponding to the sqlite rowid) pub fn get_by_id(conn: &rusqlite::Connection, id: i64) -> rusqlite::Result<Option<Self>>
pub fn get_by_id(conn: &rusqlite::Connection, id: i64) -> rusqlite::Result<Self>
where where
Self: Sized, Self: Sized,
#(#from_sql_trait_bounds),* #(#from_sql_trait_bounds),*
@ -201,20 +162,18 @@ pub fn derive_table(input: TokenStream) -> TokenStream {
let mut rows = stmt.query(rusqlite::params![id])?; let mut rows = stmt.query(rusqlite::params![id])?;
if let Some(row) = rows.next()? { if let Some(row) = rows.next()? {
Self::from_sql_row(row) Ok(Some(Self { #(#field_getters),* }))
} else { } else {
Err(rusqlite::Error::QueryReturnedNoRows) Ok(None)
} }
} }
}; };
let delete_fn = quote! { let delete_fn = quote! {
/// Delete row corresponding to the struct instance from the database. pub fn delete(&mut self, conn: &rusqlite::Connection) -> rusqlite::Result<bool>
/// Deletes the entry with rowid equal to `self.id` without further checks. where #(#to_sql_trait_bounds),*
/// {
/// Result contains `true` if a row was deleted.
pub fn delete(&mut self, conn: &rusqlite::Connection) -> rusqlite::Result<bool> {
if self.id.is_none() { if self.id.is_none() {
return Ok(false); return Ok(false);
} }
@ -228,9 +187,6 @@ pub fn derive_table(input: TokenStream) -> TokenStream {
}; };
let delete_by_id_fn = quote! { let delete_by_id_fn = quote! {
/// Delete a row from the database by rowid.
///
/// Result contains `true` if a row was deleted.
pub fn delete_by_id(conn: &rusqlite::Connection, id: i64) -> rusqlite::Result<()> { pub fn delete_by_id(conn: &rusqlite::Connection, id: i64) -> rusqlite::Result<()> {
conn.execute(&format!( conn.execute(&format!(
"DELETE FROM {} WHERE id = ?", "DELETE FROM {} WHERE id = ?",
@ -242,7 +198,6 @@ pub fn derive_table(input: TokenStream) -> TokenStream {
let drop_table_fn = quote! { let drop_table_fn = quote! {
/// Use a `Connection` to drop the table named after the struct (`#struct_name`)
pub fn drop_table(conn: &rusqlite::Connection) -> rusqlite::Result<()> { pub fn drop_table(conn: &rusqlite::Connection) -> rusqlite::Result<()> {
conn.execute(&format!("DROP TABLE {}", #table_name), [])?; conn.execute(&format!("DROP TABLE {}", #table_name), [])?;
Ok(()) Ok(())
@ -254,10 +209,9 @@ pub fn derive_table(input: TokenStream) -> TokenStream {
impl #struct_name { impl #struct_name {
#create_table_fn #create_table_fn
#insert_fn #insert_fn
#update_or_insert_fn #upsert_fn
#update_fn #update_fn
#sync_fn #sync_fn
#from_sql_row_fn
#get_by_id_fn #get_by_id_fn
#delete_fn #delete_fn
#delete_by_id_fn #delete_by_id_fn

View File

@ -1,4 +1,2 @@
pub use squail_macros::*; pub use squail_macros::*;
mod test; mod test;
pub trait SquailTable {}

View File

@ -73,7 +73,7 @@ fn test_table_derive_macro() {
let larry_copy = Person::get_by_id(&conn, larry_id).expect("Querying a row should work"); let larry_copy = Person::get_by_id(&conn, larry_id).expect("Querying a row should work");
assert_eq!( assert_eq!(
larry_copy, larry_copy,
larry.clone(), Some(larry.clone()),
"Retrieving inserted row should give an identical row" "Retrieving inserted row should give an identical row"
); );
@ -81,17 +81,14 @@ fn test_table_derive_macro() {
// also works: `Person::delete_by_id(&conn, larry_id).unwrap();` // also works: `Person::delete_by_id(&conn, larry_id).unwrap();`
assert!(deleted_something, "Should have deleted something"); assert!(deleted_something, "Should have deleted something");
let err = Person::get_by_id(&conn, larry_id) let deleted_larry = Person::get_by_id(&conn, larry_id)
.expect_err("Querying a deleted row should Err(QueryReturnedNoRows)"); .expect("Querying a deleted row should return Ok(None), not Err(_)");
assert_eq!( assert_eq!(
err, deleted_larry, None,
rusqlite::Error::QueryReturnedNoRows,
"Received row that should have been deleted" "Received row that should have been deleted"
); );
let id = larry let id = larry.upsert(&conn).expect("Upsertion (insert) should work");
.update_or_insert(&conn)
.expect("Upsertion (insert) should work");
let larry_id = larry let larry_id = larry
.id .id
.expect("After (mutable) upsertion, id should not be None"); .expect("After (mutable) upsertion, id should not be None");
@ -100,14 +97,12 @@ fn test_table_derive_macro() {
assert_eq!(id, larry_id, "Upsert should return correct id"); assert_eq!(id, larry_id, "Upsert should return correct id");
assert_eq!( assert_eq!(
larry_copy, larry_copy,
larry.clone(), Some(larry.clone()),
"Retrieving upserted row should give an identical row" "Retrieving upserted row should give an identical row"
); );
larry.age += 1; larry.age += 1;
let id = larry let id = larry.upsert(&conn).expect("Upsertion (update) should work");
.update_or_insert(&conn)
.expect("Upsertion (update) should work");
let larry_id = larry let larry_id = larry
.id .id
.expect("After (mutable) upsertion, id should not be None"); .expect("After (mutable) upsertion, id should not be None");
@ -116,7 +111,7 @@ fn test_table_derive_macro() {
let larry_copy = Person::get_by_id(&conn, larry_id).expect("Querying a row should work"); let larry_copy = Person::get_by_id(&conn, larry_id).expect("Querying a row should work");
assert_eq!( assert_eq!(
larry_copy, larry_copy,
larry.clone(), Some(larry.clone()),
"Retrieving upserted row should give an identical row" "Retrieving upserted row should give an identical row"
); );
@ -143,16 +138,6 @@ fn test_table_derive_macro() {
) )
.expect("Explicit Sqlite statement (not a library test) failed"); .expect("Explicit Sqlite statement (not a library test) failed");
assert!(!exists, "Deleted table should not exist anymore but does"); assert!(!exists, "Deleted table should not exist anymore but does");
/// Another example struct to assure this works more than once
#[derive(Table, Default)]
#[allow(unused)]
struct Car {
id: Option<i64>,
name: String,
brand: String,
year: i32,
}
} }
// TODO: implement compile-error test(s) -- perhaps with `trybuild`? // TODO: implement compile-error test(s) -- perhaps with `trybuild`?