diff --git a/butane/src/lib.rs b/butane/src/lib.rs index 8cd2691e..36b96803 100644 --- a/butane/src/lib.rs +++ b/butane/src/lib.rs @@ -12,7 +12,7 @@ pub use butane_core::many::Many; pub use butane_core::migrations; pub use butane_core::query; pub use butane_core::{ - AsPrimaryKey, DataObject, DataResult, Error, FieldType, FromSql, ObjectState, PrimaryKeyType, + AsPrimaryKey, AutoPk, DataObject, DataResult, Error, FieldType, FromSql, PrimaryKeyType, Result, SqlType, SqlVal, SqlValRef, ToSql, }; diff --git a/butane/tests/basic.rs b/butane/tests/basic.rs index 057692f5..d0f88ad6 100644 --- a/butane/tests/basic.rs +++ b/butane/tests/basic.rs @@ -1,9 +1,8 @@ #![allow(clippy::disallowed_names)] use butane::db::Connection; -use butane::{butane_type, find, model, query}; +use butane::{butane_type, find, model, query, AutoPk, ForeignKey}; use butane::{colname, prelude::*}; -use butane::{ForeignKey, ObjectState}; #[cfg(feature = "datetime")] use chrono::{naive::NaiveDateTime, offset::Utc, DateTime}; use serde::Serialize; @@ -32,7 +31,6 @@ impl Foo { bar: 0, baz: String::new(), blobbity: Vec::new(), - state: ObjectState::default(), } } } @@ -49,23 +47,20 @@ impl Bar { Bar { name: name.to_string(), foo: foo.into(), - state: ObjectState::default(), } } } #[model] struct Baz { - #[auto] - id: i64, + id: AutoPk, text: String, } impl Baz { fn new(text: &str) -> Self { Baz { - id: -1, // will be set automatically when saved + id: AutoPk::default(), text: text.to_string(), - state: ObjectState::default(), } } } @@ -76,10 +71,7 @@ struct HasOnlyPk { } impl HasOnlyPk { fn new(id: i64) -> Self { - HasOnlyPk { - id, - state: ObjectState::default(), - } + HasOnlyPk { id } } } @@ -94,7 +86,6 @@ impl SelfReferential { SelfReferential { id, reference: None, - state: ObjectState::default(), } } } @@ -349,7 +340,6 @@ fn basic_time(conn: Connection) { naive: now.naive_utc(), utc: now, when: now, - state: ObjectState::default(), }; time.save(&conn).unwrap(); diff --git a/butane/tests/common/blog.rs b/butane/tests/common/blog.rs index 8a1c4c39..cddea691 100644 --- a/butane/tests/common/blog.rs +++ b/butane/tests/common/blog.rs @@ -1,6 +1,6 @@ use butane::prelude::*; use butane::{dataresult, model}; -use butane::{db::Connection, ForeignKey, Many, ObjectState}; +use butane::{db::Connection, ForeignKey, Many}; #[cfg(feature = "datetime")] use chrono::{naive::NaiveDateTime, offset::Utc}; @@ -19,7 +19,6 @@ impl Blog { Blog { id, name: name.to_string(), - state: ObjectState::default(), } } } @@ -65,7 +64,6 @@ impl Post { likes: 0, tags: Many::new(), blog: ForeignKey::from(blog), - state: ObjectState::default(), } } } @@ -98,7 +96,6 @@ impl Tag { pub fn new(tag: &str) -> Self { Tag { tag: tag.to_string(), - state: ObjectState::default(), } } } diff --git a/butane/tests/custom_enum_derived.rs b/butane/tests/custom_enum_derived.rs index e015a7e1..170662b1 100644 --- a/butane/tests/custom_enum_derived.rs +++ b/butane/tests/custom_enum_derived.rs @@ -2,7 +2,7 @@ use butane::db::Connection; use butane::prelude::*; use butane::{model, query}; -use butane::{FieldType, FromSql, ObjectState, SqlVal, ToSql}; +use butane::{FieldType, FromSql, SqlVal, ToSql}; use butane_test_helper::*; @@ -21,11 +21,7 @@ struct HasCustomField2 { } impl HasCustomField2 { fn new(id: i64, frob: Whatsit) -> Self { - HasCustomField2 { - id, - frob, - state: ObjectState::default(), - } + HasCustomField2 { id, frob } } } diff --git a/butane/tests/custom_pg.rs b/butane/tests/custom_pg.rs index 91735be6..91af88a9 100644 --- a/butane/tests/custom_pg.rs +++ b/butane/tests/custom_pg.rs @@ -3,8 +3,8 @@ mod custom_pg { use butane::custom::{SqlTypeCustom, SqlValRefCustom}; use butane::prelude::*; - use butane::{butane_type, db::Connection, model, ObjectState}; - use butane::{FieldType, FromSql, SqlType, SqlVal, SqlValRef, ToSql}; + use butane::{butane_type, db::Connection, model}; + use butane::{AutoPk, FieldType, FromSql, SqlType, SqlVal, SqlValRef, ToSql}; use butane_test_helper::{maketest, maketest_pg}; use std::result::Result; @@ -53,18 +53,16 @@ mod custom_pg { #[model] #[derive(Debug, PartialEq)] struct Trip { - #[auto] - id: i64, + id: AutoPk, pt_from: Point, pt_to: Point, } fn roundtrip_custom(conn: Connection) { let mut trip = Trip { - id: -1, + id: AutoPk::uninitialized(), pt_from: Point::new(0.0, 0.0), pt_to: Point::new(8.0, 9.0), - state: ObjectState::default(), }; trip.save(&conn).unwrap(); @@ -81,7 +79,6 @@ mod custom_pg { id: -1, pt_from: origin.clone(), pt_to: Point::new(8.0, 9.0), - state: ObjectState::default(), }; trip1.save(&conn).unwrap(); @@ -89,7 +86,6 @@ mod custom_pg { id: -1, pt_from: Point::new(1.1, 2.0), pt_to: Point::new(7.0, 6.0), - state: ObjectState::default(), }; trip2.save(&conn).unwrap(); diff --git a/butane/tests/custom_type.rs b/butane/tests/custom_type.rs index 85fb0ab5..93a298a0 100644 --- a/butane/tests/custom_type.rs +++ b/butane/tests/custom_type.rs @@ -1,7 +1,7 @@ use butane::db::Connection; use butane::prelude::*; use butane::{butane_type, model, query}; -use butane::{FieldType, FromSql, ObjectState, SqlType, SqlVal, SqlValRef, ToSql}; +use butane::{FieldType, FromSql, SqlType, SqlVal, SqlValRef, ToSql}; use butane_test_helper::*; @@ -57,11 +57,7 @@ struct HasCustomField { } impl HasCustomField { fn new(id: i64, frob: Frobnozzle) -> Self { - HasCustomField { - id, - frob, - state: ObjectState::default(), - } + HasCustomField { id, frob } } } diff --git a/butane/tests/json.rs b/butane/tests/json.rs index b5be4caf..3d160e2f 100644 --- a/butane/tests/json.rs +++ b/butane/tests/json.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use butane::model; use butane::prelude::*; -use butane::{db::Connection, FieldType, ObjectState}; +use butane::{db::Connection, FieldType}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -23,7 +23,6 @@ impl FooJJ { id, val: Value::default(), bar: 0, - state: ObjectState::default(), } } } @@ -88,7 +87,6 @@ impl FooHH { id, val: HashMap::::default(), bar: 0, - state: ObjectState::default(), } } } @@ -133,7 +131,6 @@ impl FooHHO { id, val: HashMap::::default(), bar: 0, - state: ObjectState::default(), } } } @@ -179,11 +176,7 @@ struct OuterFoo { } impl OuterFoo { fn new(id: i64, bar: InlineFoo) -> Self { - OuterFoo { - id, - bar, - state: ObjectState::default(), - } + OuterFoo { id, bar } } } diff --git a/butane/tests/many.rs b/butane/tests/many.rs index 2d3cf006..96bdf1fc 100644 --- a/butane/tests/many.rs +++ b/butane/tests/many.rs @@ -1,6 +1,6 @@ use butane::db::Connection; use butane::prelude::*; -use butane::{model, query::OrderDirection, Many, ObjectState}; +use butane::{model, query::OrderDirection, AutoPk, Many}; use butane_test_helper::testall; @@ -12,18 +12,16 @@ use common::blog::{create_tag, Blog, Post, Tag}; #[model] struct AutoPkWithMany { - #[auto] - id: i64, + id: AutoPk, tags: Many, items: Many, } impl AutoPkWithMany { fn new() -> Self { AutoPkWithMany { - id: -1, + id: AutoPk::uninitialized(), tags: Many::default(), items: Many::default(), - state: ObjectState::default(), } } } @@ -31,26 +29,23 @@ impl AutoPkWithMany { #[model] #[table = "renamed_many_table"] struct RenamedAutoPkWithMany { - #[auto] - id: i64, + id: AutoPk, tags: Many, items: Many, } impl RenamedAutoPkWithMany { fn new() -> Self { RenamedAutoPkWithMany { - id: -1, + id: AutoPk::uninitialized(), tags: Many::default(), items: Many::default(), - state: ObjectState::default(), } } } #[model] struct AutoItem { - #[auto] - id: i64, + id: AutoPk, val: String, } @@ -192,9 +187,8 @@ testall!(can_add_to_many_before_save); fn cant_add_unsaved_to_many(_conn: Connection) { let unsaved_item = AutoItem { - id: -1, + id: AutoPk::uninitialized(), val: "shiny".to_string(), - state: ObjectState::default(), }; let mut obj = AutoPkWithMany::new(); obj.items diff --git a/butane/tests/migration-tests.rs b/butane/tests/migration-tests.rs index f2521f05..9eb5fe7c 100644 --- a/butane/tests/migration-tests.rs +++ b/butane/tests/migration-tests.rs @@ -104,8 +104,7 @@ fn current_migration_auto_attribute() { let tokens = quote! { #[derive(PartialEq, Eq, Debug, Clone)] struct Foo { - #[auto] - id: i64, + id: AutoPk, bar: String, } }; diff --git a/butane/tests/nullable.rs b/butane/tests/nullable.rs index 084f4c3c..fe691408 100644 --- a/butane/tests/nullable.rs +++ b/butane/tests/nullable.rs @@ -12,11 +12,7 @@ struct WithNullable { } impl WithNullable { fn new(id: i64) -> Self { - WithNullable { - id, - foo: None, - state: butane::ObjectState::default(), - } + WithNullable { id, foo: None } } } diff --git a/butane/tests/uuid.rs b/butane/tests/uuid.rs index 7c8c14d7..f26ae5fa 100644 --- a/butane/tests/uuid.rs +++ b/butane/tests/uuid.rs @@ -1,6 +1,6 @@ +use butane::db::Connection; use butane::model; use butane::prelude::*; -use butane::{db::Connection, ObjectState}; use uuid_for_test::Uuid; use butane_test_helper::*; @@ -13,11 +13,7 @@ struct FooUU { } impl FooUU { fn new(id: Uuid) -> Self { - FooUU { - id, - bar: 0, - state: ObjectState::default(), - } + FooUU { id, bar: 0 } } } diff --git a/butane_codegen/src/lib.rs b/butane_codegen/src/lib.rs index e104064f..e88d0f9b 100644 --- a/butane_codegen/src/lib.rs +++ b/butane_codegen/src/lib.rs @@ -28,10 +28,6 @@ mod filter; /// ## Helper Attributes /// * `#[table = "NAME"]` used on the struct to specify the name of the table (defaults to struct name) /// * `#[pk]` on a field to specify that it is the primary key. -/// * `#[auto]` on a field indicates that the field's value is -/// initialized based on serial/auto-increment. Currently supported -/// only on the primary key and only if the primary key is an integer -/// type /// * `#[unique]` on a field indicates that the field's value must be unique /// (perhaps implemented as the SQL UNIQUE constraint by some backends). /// * `[default]` should be used on fields added by later migrations to avoid errors on existing objects. @@ -42,9 +38,8 @@ mod filter; /// #[model] /// #[table = "posts"] /// pub struct Post { -/// #[auto] /// #[pk] // unnecessary if identifier were named id instead -/// pub identifier: i32, +/// pub identifier: AutoPk, /// pub title: String, /// pub content: String, /// #[default = false] diff --git a/butane_core/src/autopk.rs b/butane_core/src/autopk.rs new file mode 100644 index 00000000..f4baf429 --- /dev/null +++ b/butane_core/src/autopk.rs @@ -0,0 +1,116 @@ +//! Contains the [AutoPk] type for autoincrementing primary keys. + +use super::{FieldType, FromSql, PrimaryKeyType, Result, SqlType, SqlVal, SqlValRef, ToSql}; +use serde::{Deserialize, Serialize}; +use std::cmp::{Ordering, PartialOrd}; + +/// Wrapper around a [PrimaryKeyType] to indicate the the primary key +/// will be initialized automatically when the object is created in +/// the database. +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct AutoPk { + inner: Option, +} + +impl AutoPk { + /// Create an uninitialized value for an object which has not yet been saved. + pub fn uninitialized() -> Self + where + T: Default, + { + Self::default() + } + + /// Create an initialized primary key value for a previously saved + /// object. You do not usually need to call this directly (it will + /// happen implicitly when you load from the database). + fn with_value(val: T) -> Self { + AutoPk { inner: Some(val) } + } + + fn expect_inner(&self) -> &T { + self.inner.as_ref().expect("PK is not generated yet!") + } +} + +impl FromSql for AutoPk { + /// Used to convert a SqlValRef into another type. + fn from_sql_ref(val: SqlValRef<'_>) -> Result + where + Self: Sized, + { + Ok(AutoPk::with_value(T::from_sql_ref(val)?)) + } + + /// Used to convert a SqlVal into another type. The default + /// implementation calls `Self::from_sql_ref(val.as_ref())`, which + /// may be inefficient. This method is chiefly used only for + /// primary keys: a more efficient implementation is unlikely to + /// provide benefits for types not used as primary keys. + fn from_sql(val: SqlVal) -> Result + where + Self: Sized, + { + Ok(AutoPk::with_value(T::from_sql(val)?)) + } +} + +impl ToSql for AutoPk { + fn to_sql(&self) -> SqlVal { + self.expect_inner().to_sql() + } + fn to_sql_ref(&self) -> SqlValRef<'_> { + self.expect_inner().to_sql_ref() + } + fn into_sql(self) -> SqlVal { + self.inner.expect("PK is not generated yet!").into_sql() + } +} + +impl PartialEq for AutoPk { + fn eq(&self, other: &AutoPk) -> bool { + if !self.is_valid() || !other.is_valid() { + false + } else { + self.inner.eq(&other.inner) + } + } +} + +impl FieldType for AutoPk { + const SQLTYPE: SqlType = T::SQLTYPE; + /// Reference type. Used for ergonomics with String (which has + /// reference type str). For most, it is Self. + type RefType = T::RefType; +} +impl PrimaryKeyType for AutoPk { + fn is_valid(&self) -> bool { + match &self.inner { + Some(val) => val.is_valid(), + None => false, + } + } +} + +impl std::fmt::Display for AutoPk { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + match &self.inner { + Some(val) => val.fmt(f), + None => write!(f, "UNINITIALIZED"), + } + } +} + +impl Copy for AutoPk {} + +impl PartialOrd for AutoPk { + fn partial_cmp(&self, other: &Self) -> Option { + match &self.inner { + Some(val) => match &other.inner { + Some(val2) => val.partial_cmp(val2), + None => None, + }, + None => None, + } + } +} diff --git a/butane_core/src/codegen/dbobj.rs b/butane_core/src/codegen/dbobj.rs index b47fda23..3a86740b 100644 --- a/butane_core/src/codegen/dbobj.rs +++ b/butane_core/src/codegen/dbobj.rs @@ -29,12 +29,44 @@ pub fn impl_dbobject(ast_struct: &ItemStruct, config: &Config) -> TokenStream2 { let pklit = make_ident_literal_str(&pkident); let auto_pk = is_auto(&pk_field); + let values: Vec = push_values(ast_struct, |_| true); let insert_cols = columns(ast_struct, |f| !is_auto(f)); - let save_cols = columns(ast_struct, |f| !is_auto(f) && f != &pk_field); - let mut post_insert: Vec = Vec::new(); - add_post_insert_for_auto(&pk_field, &mut post_insert); - post_insert.push(quote!(self.state.saved = true;)); + let save_core = if is_auto(&pk_field) { + let pkident = pk_field.ident.clone().unwrap(); + let values_no_pk: Vec = push_values(ast_struct, |f: &Field| f != &pk_field); + let save_cols = columns(ast_struct, |f| !is_auto(f) && f != &pk_field); + quote!( + // Since we expect our pk field to be invalid and to be created by the insert, + // we do a pure insert or update based on whether the AutoPk is already valid or not. + // Note that some database backends do support upsert with auto-incrementing primary + // keys, but butane isn't well set up to take advantage of that, including missing + // support for constraints and the `insert_or_update` method not providing a way to + // retrieve the pk. + if (butane::PrimaryKeyType::is_valid(&self.#pkident)) { + #(#values_no_pk)* + if values.len() > 0 { + conn.update( + Self::TABLE, + pkcol, + butane::ToSql::to_sql_ref(self.pk()), + &[#save_cols], + &values, + )?; + } + } else { + #(#values)* + let pk = conn.insert_returning_pk(Self::TABLE, &[#insert_cols], &pkcol, &values)?; + self.#pkident = butane::FromSql::from_sql(pk)?; + } + ) + } else { + // do an upsert + quote!( + #(#values)* + conn.insert_or_replace(Self::TABLE, &[#insert_cols], &pkcol, &values)?; + ) + }; let numdbfields = fields(ast_struct).filter(|f| is_row_field(f)).count(); let many_save: TokenStream2 = fields(ast_struct) @@ -56,9 +88,6 @@ pub fn impl_dbobject(ast_struct: &ItemStruct, config: &Config) -> TokenStream2 { }) .collect(); - let values: Vec = push_values(ast_struct, |_| true); - let values_no_pk: Vec = push_values(ast_struct, |f: &Field| f != &pk_field); - let dataresult = impl_dataresult(ast_struct, tyname, config); quote!( #dataresult @@ -77,35 +106,7 @@ pub fn impl_dbobject(ast_struct: &ItemStruct, config: &Config) -> TokenStream2 { let pkcol = butane::db::Column::new( #pklit, <#pktype as butane::FieldType>::SQLTYPE); - if self.state.saved { - // Already exists in db, do an update - #(#values_no_pk)* - if values.len() > 0 { - conn.update( - Self::TABLE, - pkcol, - butane::ToSql::to_sql_ref(self.pk()), - &[#save_cols], - &values, - )?; - } - } else if #auto_pk { - // Since we expect our pk field to be invalid and to be created by the insert, - // we do a pure insert, no upsert allowed. - #(#values)* - let pk = conn.insert_returning_pk( - Self::TABLE, - &[#insert_cols], - &pkcol, - &values, - )?; - #(#post_insert)* - } else { - // Do an upsert - #(#values)* - conn.insert_or_replace(Self::TABLE, &[#insert_cols], &pkcol, &values)?; - self.state.saved = true - } + #save_core #many_save Ok(()) } @@ -114,10 +115,6 @@ pub fn impl_dbobject(ast_struct: &ItemStruct, config: &Config) -> TokenStream2 { use butane::prelude::DataObject; conn.delete(Self::TABLE, Self::PKCOL, self.pk().to_sql()) } - - fn is_saved(&self) -> butane::Result { - Ok(self.state.saved) - } } impl butane::ToSql for #tyname { fn to_sql(&self) -> butane::SqlVal { @@ -192,10 +189,8 @@ pub fn impl_dataresult(ast_struct: &ItemStruct, dbo: &Ident, config: &Config) -> let ctor = if dbo_is_self { quote!( let mut obj = #tyname { - state: butane::ObjectState::default(), #(#rows),* }; - obj.state.saved = true; ) } else { quote!( @@ -281,7 +276,7 @@ fn fieldexpr_func_regular(f: &Field, ast_struct: &ItemStruct) -> TokenStream2 { fn fieldexpr_func_many(f: &Field, ast_struct: &ItemStruct, config: &Config) -> TokenStream2 { let tyname = &ast_struct.ident; - let fty = get_foreign_type_argument(&f.ty, &MANY_TYNAMES).expect("Many field misdetected"); + let fty = get_type_argument(&f.ty, &MANY_TYNAMES).expect("Many field misdetected"); let many_table_lit = many_table_lit(ast_struct, f, config); fieldexpr_func( f, @@ -395,7 +390,7 @@ fn verify_fields(ast_struct: &ItemStruct) -> Option { let pk_field = pk_field.unwrap(); for f in fields(ast_struct) { if is_auto(f) { - match get_primitive_sql_type(&f.ty) { + match get_autopk_sql_type(&f.ty) { Some(DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::Int))) => (), Some(DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::BigInt))) => (), _ => { @@ -415,14 +410,6 @@ fn verify_fields(ast_struct: &ItemStruct) -> Option { None } -fn add_post_insert_for_auto(pk_field: &Field, post_insert: &mut Vec) { - if !is_auto(pk_field) { - return; - } - let pkident = pk_field.ident.clone().unwrap(); - post_insert.push(quote!(self.#pkident = butane::FromSql::from_sql(pk)?;)); -} - /// Builds code for pushing SqlVals for each column satisfying predicate into a vec called `values` fn push_values

(ast_struct: &ItemStruct, mut predicate: P) -> Vec where diff --git a/butane_core/src/codegen/mod.rs b/butane_core/src/codegen/mod.rs index 8edf1717..bb6789fd 100644 --- a/butane_core/src/codegen/mod.rs +++ b/butane_core/src/codegen/mod.rs @@ -16,6 +16,7 @@ use syn::{ const OPTION_TYNAMES: [&str; 2] = ["Option", "std::option::Option"]; const MANY_TYNAMES: [&str; 2] = ["Many", "butane::Many"]; const FKEY_TYNAMES: [&str; 2] = ["ForeignKey", "butane::ForeignKey"]; +const AUTOPK_TYNAMES: [&str; 2] = ["AutoPk", "butane::AutoPk"]; #[macro_export] macro_rules! make_compile_error { @@ -49,12 +50,6 @@ where // Filter out our helper attributes let attrs: Vec = filter_helper_attributes(&ast_struct); - let state_attrs = if has_derive_serialize(&attrs) { - quote!(#[serde(skip)]) - } else { - TokenStream2::new() - }; - let vis = &ast_struct.vis; migration::write_table_to_disk(ms, &ast_struct, &config).unwrap(); @@ -68,16 +63,11 @@ where Err(err) => return err, }; - // If the program already declared a state field, remove it - let fields = remove_existing_state_field(fields); - let ident = ast_struct.ident; quote!( #(#attrs)* #vis struct #ident { - #state_attrs - pub state: butane::ObjectState, #fields } #impltraits @@ -95,12 +85,6 @@ pub fn dataresult(args: TokenStream2, input: TokenStream2) -> TokenStream2 { // Filter out our helper attributes let attrs: Vec = filter_helper_attributes(&ast_struct); - let state_attrs = if has_derive_serialize(&attrs) { - quote!(#[serde(skip)]) - } else { - TokenStream2::new() - }; - let vis = &ast_struct.vis; let impltraits = dbobj::impl_dataresult(&ast_struct, &dbo, &config); @@ -115,7 +99,6 @@ pub fn dataresult(args: TokenStream2, input: TokenStream2) -> TokenStream2 { quote!( #(#attrs)* #vis struct #ident { - #state_attrs #fields } #impltraits @@ -266,28 +249,6 @@ fn remove_helper_field_attributes( } } -// We allow model structs to declare the state: butane::ObjectState -// field for convenience so it doesn't appear so magical, but then we -// recreate it. -fn remove_existing_state_field( - fields: Punctuated, -) -> Punctuated { - fields - .into_iter() - .filter(|f| match (&f.ident, &f.ty) { - (Some(ident), syn::Type::Path(typ)) => { - ident != "state" - || typ - .path - .segments - .last() - .map_or(true, |seg| seg.ident != "ObjectState") - } - (_, _) => true, - }) - .collect() -} - fn pk_field(ast_struct: &ItemStruct) -> Option { let pk_by_attribute = fields(ast_struct).find(|f| f.attrs.iter().any(|attr| attr.path().is_ident("pk"))); @@ -302,7 +263,7 @@ fn pk_field(ast_struct: &ItemStruct) -> Option { } fn is_auto(field: &Field) -> bool { - field.attrs.iter().any(|attr| attr.path().is_ident("auto")) + get_type_argument(&field.ty, &AUTOPK_TYNAMES).is_some() } fn is_unique(field: &Field) -> bool { @@ -313,14 +274,11 @@ fn is_unique(field: &Field) -> bool { } fn fields(ast_struct: &ItemStruct) -> impl Iterator { - ast_struct - .fields - .iter() - .filter(|f| f.ident.clone().unwrap() != "state") + ast_struct.fields.iter() } fn get_option_sql_type(ty: &syn::Type) -> Option { - get_foreign_type_argument(ty, &OPTION_TYNAMES).map(|path| { + get_type_argument(ty, &OPTION_TYNAMES).map(|path| { let inner_ty: syn::Type = syn::TypePath { qself: None, path: path.clone(), @@ -335,12 +293,24 @@ fn get_many_sql_type(field: &Field) -> Option { get_foreign_sql_type(&field.ty, &MANY_TYNAMES) } +fn get_autopk_sql_type(ty: &syn::Type) -> Option { + get_type_argument(ty, &AUTOPK_TYNAMES).map(|path| { + let inner_ty: syn::Type = syn::TypePath { + qself: None, + path: path.clone(), + } + .into(); + + get_deferred_sql_type(&inner_ty) + }) +} + fn is_many_to_many(field: &Field) -> bool { get_many_sql_type(field).is_some() } fn is_option(field: &Field) -> bool { - get_foreign_type_argument(&field.ty, &OPTION_TYNAMES).is_some() + get_type_argument(&field.ty, &OPTION_TYNAMES).is_some() } /// Check for special fields which won't correspond to rows and don't @@ -362,10 +332,9 @@ fn is_same_path_ident(path1: &syn::Path, path2: &syn::Path) -> bool { .all(|(a, b)| a.ident == b.ident) } -fn get_foreign_type_argument<'a>( - ty: &'a syn::Type, - tynames: &[&'static str], -) -> Option<&'a syn::Path> { +/// Gets the type argument of a type. +/// E.g. for Foo, returns T +fn get_type_argument<'a>(ty: &'a syn::Type, tynames: &[&'static str]) -> Option<&'a syn::Path> { let path = match ty { syn::Type::Path(path) => &path.path, _ => return None, @@ -395,7 +364,7 @@ fn get_foreign_type_argument<'a>( } fn get_foreign_sql_type(ty: &syn::Type, tynames: &[&'static str]) -> Option { - let typath = get_foreign_type_argument(ty, tynames); + let typath = get_type_argument(ty, tynames); typath.map(|typath| { DeferredSqlType::Deferred(TypeKey::PK( typath @@ -415,6 +384,7 @@ pub fn get_deferred_sql_type(ty: &syn::Type) -> DeferredSqlType { get_primitive_sql_type(ty) .or_else(|| get_option_sql_type(ty)) .or_else(|| get_foreign_sql_type(ty, &FKEY_TYNAMES)) + .or_else(|| get_autopk_sql_type(ty)) .unwrap_or_else(|| { DeferredSqlType::Deferred(TypeKey::CustomType( ty.clone().into_token_stream().to_string(), @@ -545,25 +515,6 @@ fn template_type(arguments: &syn::PathArguments) -> Option<&Ident> { None } -fn has_derive_serialize(attrs: &[Attribute]) -> bool { - for attr in attrs { - if attr.path().is_ident("derive") { - let mut result = false; - attr.parse_nested_meta(|meta| { - result |= meta - .path - .segments - .iter() - .any(|segment| segment.ident == "Serialize"); - Ok(()) - }) - .unwrap(); - return result; - } - } - false -} - fn sqlval_from_lit(lit: Lit) -> std::result::Result { match lit { Lit::Str(lit) => Ok(SqlVal::Text(lit.value())), @@ -649,51 +600,51 @@ mod tests { use super::*; #[test] - fn test_get_foreign_type_argument_option() { + fn test_get_type_argument_option() { let expected_type_path: syn::Path = syn::parse_quote!(butane::ForeignKey); let type_path: syn::TypePath = syn::parse_quote!(Option>); let typ = syn::Type::Path(type_path); - let rv = get_foreign_type_argument(&typ, &OPTION_TYNAMES); + let rv = get_type_argument(&typ, &OPTION_TYNAMES); assert!(rv.is_some()); assert_eq!(rv.unwrap(), &expected_type_path); let type_path: syn::TypePath = syn::parse_quote!(butane::ForeignKey); let typ = syn::Type::Path(type_path); - let rv = get_foreign_type_argument(&typ, &OPTION_TYNAMES); + let rv = get_type_argument(&typ, &OPTION_TYNAMES); assert!(rv.is_none()); } #[test] - fn test_get_foreign_type_argument_fky() { + fn test_get_type_argument_fky() { let expected_type_path: syn::Path = syn::parse_quote!(Foo); let type_path: syn::TypePath = syn::parse_quote!(butane::ForeignKey); let typ = syn::Type::Path(type_path); - let rv = get_foreign_type_argument(&typ, &FKEY_TYNAMES); + let rv = get_type_argument(&typ, &FKEY_TYNAMES); assert!(rv.is_some()); assert_eq!(rv.unwrap(), &expected_type_path); let type_path: syn::TypePath = syn::parse_quote!(Foo); let typ = syn::Type::Path(type_path); - let rv = get_foreign_type_argument(&typ, &FKEY_TYNAMES); + let rv = get_type_argument(&typ, &FKEY_TYNAMES); assert!(rv.is_none()); } #[test] - fn test_get_foreign_type_argument_many() { + fn test_get_type_argument_many() { let expected_type_path: syn::Path = syn::parse_quote!(Foo); let type_path: syn::TypePath = syn::parse_quote!(butane::Many); let typ = syn::Type::Path(type_path); - let rv = get_foreign_type_argument(&typ, &MANY_TYNAMES); + let rv = get_type_argument(&typ, &MANY_TYNAMES); assert!(rv.is_some()); assert_eq!(rv.unwrap(), &expected_type_path); let type_path: syn::TypePath = syn::parse_quote!(Foo); let typ = syn::Type::Path(type_path); - let rv = get_foreign_type_argument(&typ, &MANY_TYNAMES); + let rv = get_type_argument(&typ, &MANY_TYNAMES); assert!(rv.is_none()); } } diff --git a/butane_core/src/fkey.rs b/butane_core/src/fkey.rs index 5ca011b2..560f7ab7 100644 --- a/butane_core/src/fkey.rs +++ b/butane_core/src/fkey.rs @@ -66,7 +66,7 @@ impl ForeignKey { self.val .get_or_try_init(|| { let pk = self.valpk.get().unwrap(); - T::get(conn, &T::PKType::from_sql_ref(pk.as_ref())?).map(Box::new) + T::get(conn, T::PKType::from_sql_ref(pk.as_ref())?).map(Box::new) }) .map(|v| v.as_ref()) } diff --git a/butane_core/src/lib.rs b/butane_core/src/lib.rs index 10a69c5f..3a930f3c 100644 --- a/butane_core/src/lib.rs +++ b/butane_core/src/lib.rs @@ -1,7 +1,6 @@ #![allow(clippy::iter_nth_zero)] #![allow(clippy::upper_case_acronyms)] //grandfathered, not going to break API to rename use serde::{Deserialize, Serialize}; -use std::borrow::Borrow; use std::cmp::{Eq, PartialEq}; use std::default::Default; use thiserror::Error as ThisError; @@ -18,8 +17,8 @@ pub mod sqlval; #[cfg(feature = "uuid")] pub mod uuid; -#[cfg(feature = "fake")] -use fake::{Dummy, Faker}; +mod autopk; +pub use autopk::AutoPk; use db::{BackendRow, Column, ConnectionMethods}; @@ -29,34 +28,6 @@ pub use sqlval::*; pub type Result = std::result::Result; -/// Used internally by butane to track state about the object. -/// -/// Includes information such as whether it has actually been created -/// in the database yet. Butane automatically creates the field -/// `state: ObjectState` on `#[model]` structs. When initializing the -/// state field, use `ObjectState::default()`. -#[derive(Clone, Debug, Default, Ord, PartialOrd)] -pub struct ObjectState { - pub saved: bool, -} -/// Two `ObjectState`s always compare as equal. This effectively -/// removes `ObjectState` from participating in equality tests between -/// two objects -impl PartialEq for ObjectState { - fn eq(&self, _other: &ObjectState) -> bool { - true - } -} -impl Eq for ObjectState {} - -#[cfg(feature = "fake")] -/// Fake data should always have `saved` set to `false`. -impl Dummy for ObjectState { - fn dummy_with_rng(_: &Faker, _rng: &mut R) -> Self { - Self::default() - } -} - /// A type which may be the result of a database query. /// /// Every result type must have a corresponding object type and the @@ -94,7 +65,7 @@ pub trait DataObject: DataResult { fn pk(&self) -> &Self::PKType; /// Find this object in the database based on primary key. /// Returns `Error::NoSuchObject` if the primary key does not exist. - fn get(conn: &impl ConnectionMethods, id: impl Borrow) -> Result + fn get(conn: &impl ConnectionMethods, id: impl ToSql) -> Result where Self: Sized, { @@ -102,14 +73,14 @@ pub trait DataObject: DataResult { } /// Find this object in the database based on primary key. /// Returns `None` if the primary key does not exist. - fn try_get(conn: &impl ConnectionMethods, id: impl Borrow) -> Result> + fn try_get(conn: &impl ConnectionMethods, id: impl ToSql) -> Result> where Self: Sized, { Ok(::query() .filter(query::BoolExpr::Eq( Self::PKCOL, - query::Expr::Val(id.borrow().to_sql()), + query::Expr::Val(id.to_sql()), )) .limit(1) .load(conn)? @@ -120,9 +91,6 @@ pub trait DataObject: DataResult { fn save(&mut self, conn: &impl ConnectionMethods) -> Result<()>; /// Delete the object from the database. fn delete(&self, conn: &impl ConnectionMethods) -> Result<()>; - - /// Test if this object has been saved to the database at least once - fn is_saved(&self) -> Result; } pub trait ModelTyped { diff --git a/butane_core/src/many.rs b/butane_core/src/many.rs index 34589e63..8dd3cef3 100644 --- a/butane_core/src/many.rs +++ b/butane_core/src/many.rs @@ -2,7 +2,7 @@ #![deny(missing_docs)] use crate::db::{Column, ConnectionMethods}; use crate::query::{BoolExpr, Expr, OrderDirection, Query}; -use crate::{DataObject, Error, FieldType, Result, SqlType, SqlVal, ToSql}; +use crate::{DataObject, Error, FieldType, PrimaryKeyType, Result, SqlType, SqlVal, ToSql}; use once_cell::unsync::OnceCell; use serde::{Deserialize, Serialize}; use std::borrow::Cow; @@ -74,11 +74,8 @@ where /// to have an uninitialized one. pub fn add(&mut self, new_val: &T) -> Result<()> { // Check for uninitialized pk - match new_val.is_saved() { - Ok(true) => (), // hooray - Ok(false) => return Err(Error::ValueNotSaved), - Err(Error::SaveDeterminationNotSupported) => (), // we don't know, so assume it's OK - Err(e) => return Err(e), // unexpected error + if !new_val.pk().is_valid() { + return Err(Error::ValueNotSaved); } // all_values is now out of date, so clear it diff --git a/butane_core/src/migrations/mod.rs b/butane_core/src/migrations/mod.rs index 2fef93c8..30758404 100644 --- a/butane_core/src/migrations/mod.rs +++ b/butane_core/src/migrations/mod.rs @@ -275,9 +275,4 @@ impl DataObject for ButaneMigration { fn delete(&self, conn: &impl ConnectionMethods) -> Result<()> { conn.delete(Self::TABLE, Self::PKCOL, self.pk().to_sql()) } - fn is_saved(&self) -> Result { - // In practice we don't expect this to be called as - // ButaneMigration is not exposed outside the library - Err(Error::SaveDeterminationNotSupported) - } } diff --git a/butane_core/src/sqlval.rs b/butane_core/src/sqlval.rs index c0a764f4..2a32cd87 100644 --- a/butane_core/src/sqlval.rs +++ b/butane_core/src/sqlval.rs @@ -287,7 +287,16 @@ pub trait FieldType: ToSql + FromSql { } /// Marker trait for a type suitable for being a primary key -pub trait PrimaryKeyType: FieldType + Clone + PartialEq {} +pub trait PrimaryKeyType: FieldType + Clone + PartialEq { + /// Test if this object's pk is valid. The only case in which this + /// returns false is if the pk is an AutoPk and it's not yet valid. + /// + /// If you're implementing [PrimaryKeyType], the default implementation returns true + /// and you do not need to change that unless you're doing something very unusual. + fn is_valid(&self) -> bool { + true + } +} /// Trait for referencing the primary key for a given model. Used to /// implement ForeignKey equality tests. diff --git a/docs/getting-started.md b/docs/getting-started.md index 2a5c9056..dcf6846f 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -67,15 +67,14 @@ use butane::{model, ForeignKey, Many, ObjectState}; #[model] #[derive(Debug, Default)] pub struct Blog { - #[auto] - pub id: i64, + pub id: AutoPk, pub name: String, } impl Blog { pub fn new(name: impl Into) -> Self { Blog { - name: name.into(), - ..Default::default() + id: AutoPk::uninitialized(), + name: name.into() } } } @@ -98,13 +97,13 @@ The `#[model]` attribute does the heavy lifting here: The `id` field is special -- it's the primary key. All models must have a primary key. If we didn't want to name ours `id`, we could have added a `#[pk]` attribute to denote the primary key field. The -`#[auto]` attribute says that the field should be populated +`AutoPk` wrapping type says that the field should be populated automatically from an incrementing value. It is only allowed on integer types and will cause the underlying column to be `AUTOINCREMENT` for SQLite or `SERIAL`/`BIGSERIAL` for -PostgreSQL. Since it's marked as `#[auto]` the value of `id` at -construction time doesn't matter: it will be automatically set when -the object is created (via its [`save`] method). +PostgreSQL. When the object is created in the database via its +[`save`] method, the `AutoPk` field will be updated to its initialized +value. Now let's add a model to represent a blog post, and in the process take a look at a few more features. diff --git a/example/src/main.rs b/example/src/main.rs index d87b45e0..8027a2d9 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -1,10 +1,6 @@ #![allow(dead_code)] use butane::db::{Connection, ConnectionSpec}; -use butane::model; -use butane::Error; -use butane::ObjectState; -use butane::{find, query}; -use butane::{ForeignKey, Many}; +use butane::{find, model, query, AutoPk, Error, ForeignKey, Many}; use butane::prelude::*; @@ -13,16 +9,14 @@ type Result = std::result::Result; #[model] #[derive(Debug, Default)] struct Blog { - #[auto] - id: i64, + id: AutoPk, name: String, } #[model] #[derive(Debug)] struct Post { - #[auto] - id: i64, + id: AutoPk, title: String, body: String, published: bool, @@ -34,7 +28,7 @@ struct Post { impl Post { fn new(blog: &Blog, title: String, body: String) -> Self { Post { - id: -1, + id: AutoPk::default(), title, body, published: false, @@ -42,7 +36,6 @@ impl Post { blog: blog.into(), byline: None, likes: 0, - state: ObjectState::default(), } } } @@ -64,7 +57,6 @@ fn query() -> Result<()> { let mut tag = Tag { tag: "dinosaurs".into(), - ..Default::default() }; tag.save(&conn).unwrap(); diff --git a/examples/getting_started/src/models.rs b/examples/getting_started/src/models.rs index 342907fc..c5655bbf 100644 --- a/examples/getting_started/src/models.rs +++ b/examples/getting_started/src/models.rs @@ -1,15 +1,15 @@ //! Models for the getting_started example. use butane::prelude::*; -use butane::{model, ForeignKey, Many, ObjectState}; +use butane::AutoPk; +use butane::{model, ForeignKey, Many}; /// Blog metadata. #[model] #[derive(Debug, Default)] pub struct Blog { /// Id of the blog. - #[auto] - pub id: i64, + pub id: AutoPk, /// Name of the blog. pub name: String, } @@ -28,8 +28,7 @@ impl Blog { #[model] pub struct Post { /// Id of the blog post. - #[auto] - pub id: i32, + pub id: AutoPk, /// Title of the blog post. pub title: String, /// Body of the blog post. @@ -44,14 +43,12 @@ pub struct Post { pub byline: Option, /// How many likes this post has. pub likes: i32, - /// Explicit internal butane state field. - state: butane::ObjectState, } impl Post { /// Create a new Post. pub fn new(blog: &Blog, title: String, body: String) -> Self { Post { - id: -1, + id: AutoPk::uninitialized(), title, body, published: false, @@ -59,7 +56,6 @@ impl Post { blog: blog.into(), byline: None, likes: 0, - state: ObjectState::default(), } } } @@ -75,9 +71,6 @@ pub struct Tag { impl Tag { /// Create a new Tag. pub fn new(tag: impl Into) -> Self { - Tag { - tag: tag.into(), - ..Default::default() - } + Tag { tag: tag.into() } } }