diff --git a/cot-cli/src/migration_generator.rs b/cot-cli/src/migration_generator.rs index 4fc8a6a33..2053453ea 100644 --- a/cot-cli/src/migration_generator.rs +++ b/cot-cli/src/migration_generator.rs @@ -1325,8 +1325,13 @@ impl Repr for Field { let column_name = &self.column_name; let ty = &self.ty; let mut tokens = quote! { - ::cot::db::migrations::Field::new(::cot::db::Identifier::new(#column_name), <#ty as ::cot::db::DatabaseField>::TYPE) + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new(#column_name), + <#ty as ::cot::db::DatabaseField>::TYPE, + ) + .set_null(<#ty as ::cot::db::DatabaseField>::NULLABLE) }; + if self.auto_value { tokens = quote! { #tokens.auto() } } @@ -1335,17 +1340,18 @@ impl Repr for Field { } if let Some(fk_spec) = self.foreign_key.clone() { let to_model = &fk_spec.to_model; + let on_delete = fk_spec.on_delete.unwrap_or_default(); + let on_update = fk_spec.on_update.unwrap_or_default(); tokens = quote! { #tokens.foreign_key( <#to_model as ::cot::db::Model>::TABLE_NAME, <#to_model as ::cot::db::Model>::PRIMARY_KEY_NAME, - ::cot::db::ForeignKeyOnDeletePolicy::Restrict, - ::cot::db::ForeignKeyOnUpdatePolicy::Restrict, + #on_delete, + #on_update, ) } } - tokens = quote! { #tokens.set_null(<#ty as ::cot::db::DatabaseField>::NULLABLE) }; if self.unique { tokens = quote! { #tokens.unique() } } @@ -1547,10 +1553,67 @@ impl Error for ParsingError {} #[cfg(test)] mod tests { - use cot_codegen::model::ForeignKeySpec; + use cot_codegen::model::{ForeignKeyOnDeletePolicy, ForeignKeyOnUpdatePolicy, ForeignKeySpec}; use super::*; + fn remove_whitespace>(s: &T) -> String { + s.as_ref().chars().filter(|c| !c.is_whitespace()).collect() + } + + macro_rules! repr_for_foreign_key_operation_test { + ($test_name:ident, $on_delete:path, $on_update:path) => { + #[test] + fn $test_name() { + let op = DynOperation::AddField { + table_name: "test_table".to_string(), + model_ty: parse_quote!(TestModel), + field: Box::new(Field { + name: format_ident!("test_field"), + column_name: "test_field".to_string(), + ty: parse_quote!(cot::db::ForeignKey), + auto_value: false, + primary_key: false, + unique: false, + foreign_key: Some(ForeignKeySpec { + to_model: parse_quote!(crate::OtherModel), + on_delete: Some($on_delete), + on_update: Some($on_update), + }), + }), + }; + + let tokens = op.repr(); + let tokens_str = remove_whitespace(&tokens.to_string()); + + let expected_str = format!( + r#" + ::cot::db::migrations::Field::new( + ::cot::db::Identifier::new("test_field"), + as ::cot::db::DatabaseField>::TYPE, + ) + .set_null( + as ::cot::db::DatabaseField>::NULLABLE + ) + .foreign_key( + ::TABLE_NAME, + ::PRIMARY_KEY_NAME, + ::cot::db::{}, + ::cot::db::{}, + ) + "#, + stringify!($on_delete), + stringify!($on_update), + ); + assert!(tokens_str.contains(&remove_whitespace(&expected_str))); + } + }; + } + #[test] fn migration_processor_next_migration_name_empty() { let migrations = vec![]; @@ -1639,6 +1702,8 @@ mod tests { unique: false, foreign_key: Some(ForeignKeySpec { to_model: parse_quote!(Table1), + on_delete: Some(ForeignKeyOnDeletePolicy::Cascade), + on_update: Some(ForeignKeyOnUpdatePolicy::Cascade), }), }), }, @@ -1679,6 +1744,8 @@ mod tests { unique: false, foreign_key: Some(ForeignKeySpec { to_model: parse_quote!(Table2), + on_delete: Some(ForeignKeyOnDeletePolicy::Cascade), + on_update: Some(ForeignKeyOnUpdatePolicy::Cascade), }), }], }, @@ -1694,6 +1761,8 @@ mod tests { unique: false, foreign_key: Some(ForeignKeySpec { to_model: parse_quote!(Table1), + on_delete: Some(ForeignKeyOnDeletePolicy::Cascade), + on_update: Some(ForeignKeyOnUpdatePolicy::Cascade), }), }], }, @@ -1741,6 +1810,8 @@ mod tests { unique: false, foreign_key: Some(ForeignKeySpec { to_model: parse_quote!(Table2), + on_delete: Some(ForeignKeyOnDeletePolicy::Cascade), + on_update: Some(ForeignKeyOnUpdatePolicy::Cascade), }), }], }; @@ -1796,6 +1867,8 @@ mod tests { unique: false, foreign_key: Some(ForeignKeySpec { to_model: parse_quote!(crate::Table2), + on_delete: Some(ForeignKeyOnDeletePolicy::Cascade), + on_update: Some(ForeignKeyOnUpdatePolicy::Cascade), }), }], }]; @@ -1825,6 +1898,8 @@ mod tests { unique: false, foreign_key: Some(ForeignKeySpec { to_model: parse_quote!(my_crate::Table2), + on_delete: Some(ForeignKeyOnDeletePolicy::Cascade), + on_update: Some(ForeignKeyOnUpdatePolicy::Cascade), }), }], }, @@ -1840,6 +1915,8 @@ mod tests { unique: false, foreign_key: Some(ForeignKeySpec { to_model: parse_quote!(crate::Table4), + on_delete: Some(ForeignKeyOnDeletePolicy::Cascade), + on_update: Some(ForeignKeyOnUpdatePolicy::Cascade), }), }], }, @@ -2125,6 +2202,30 @@ mod tests { "Should call build() but got: {tokens_str}" ); } + + repr_for_foreign_key_operation_test!( + repr_for_foreign_key_operation_cascade_cascade, + ForeignKeyOnDeletePolicy::Cascade, + ForeignKeyOnUpdatePolicy::Cascade + ); + + repr_for_foreign_key_operation_test!( + repr_for_foreign_key_operation_restrict_restrict, + ForeignKeyOnDeletePolicy::Restrict, + ForeignKeyOnUpdatePolicy::Restrict + ); + repr_for_foreign_key_operation_test!( + repr_for_foreign_key_operation_cascade_noaction, + ForeignKeyOnDeletePolicy::Cascade, + ForeignKeyOnUpdatePolicy::NoAction + ); + + repr_for_foreign_key_operation_test!( + repr_for_foreign_key_operation_noaction_restrict, + ForeignKeyOnDeletePolicy::NoAction, + ForeignKeyOnUpdatePolicy::Restrict + ); + #[test] fn generate_operations_with_removed_field() { let app_model = get_test_model(); diff --git a/cot-codegen/src/model.rs b/cot-codegen/src/model.rs index d548072e4..ea3f2b8a5 100644 --- a/cot-codegen/src/model.rs +++ b/cot-codegen/src/model.rs @@ -1,5 +1,7 @@ use darling::{FromDeriveInput, FromField, FromMeta}; use heck::ToSnakeCase; +use proc_macro2::TokenStream; +use quote::{ToTokens, quote}; use syn::ext::IdentExt; use syn::spanned::Spanned; @@ -139,6 +141,72 @@ impl ModelOpts { } } +#[derive(Debug, Clone, Copy, Default, FromMeta)] +pub struct ForeignKeyArgs { + #[darling(default)] + pub on_delete: ForeignKeyOnDeletePolicy, + #[darling(default)] + pub on_update: ForeignKeyOnUpdatePolicy, +} + +#[derive(Debug, Clone, Copy, Default, FromMeta, PartialEq, Eq, Hash)] +pub enum ForeignKeyOnDeletePolicy { + #[default] + Restrict, + Cascade, + SetNone, + NoAction, +} + +impl ToTokens for ForeignKeyOnDeletePolicy { + fn to_tokens(&self, tokens: &mut TokenStream) { + let variant = match self { + ForeignKeyOnDeletePolicy::Restrict => { + quote! {::cot::db::ForeignKeyOnDeletePolicy::Restrict} + } + ForeignKeyOnDeletePolicy::Cascade => { + quote! {::cot::db::ForeignKeyOnDeletePolicy::Cascade} + } + ForeignKeyOnDeletePolicy::SetNone => { + quote! {::cot::db::ForeignKeyOnDeletePolicy::SetNone} + } + ForeignKeyOnDeletePolicy::NoAction => { + quote! {::cot::db::ForeignKeyOnDeletePolicy::NoAction} + } + }; + variant.to_tokens(tokens); + } +} + +#[derive(Debug, Clone, Copy, Default, FromMeta, PartialEq, Eq, Hash)] +pub enum ForeignKeyOnUpdatePolicy { + Restrict, + #[default] + Cascade, + SetNone, + NoAction, +} + +impl ToTokens for ForeignKeyOnUpdatePolicy { + fn to_tokens(&self, tokens: &mut TokenStream) { + let variant = match self { + ForeignKeyOnUpdatePolicy::Restrict => { + quote! {::cot::db::ForeignKeyOnUpdatePolicy::Restrict} + } + ForeignKeyOnUpdatePolicy::Cascade => { + quote! {::cot::db::ForeignKeyOnUpdatePolicy::Cascade} + } + ForeignKeyOnUpdatePolicy::SetNone => { + quote! {::cot::db::ForeignKeyOnUpdatePolicy::SetNone} + } + ForeignKeyOnUpdatePolicy::NoAction => { + quote! {::cot::db::ForeignKeyOnUpdatePolicy::NoAction} + } + }; + variant.to_tokens(tokens); + } +} + #[derive(Debug, Clone, FromField)] #[darling(attributes(model))] pub struct FieldOpts { @@ -147,6 +215,7 @@ pub struct FieldOpts { pub primary_key: darling::util::Flag, pub unique: darling::util::Flag, pub field_name: Option, + pub foreign_key: Option, } impl FieldOpts { @@ -195,6 +264,37 @@ impl FieldOpts { }) } + fn resolved_type_name(&self, symbol_resolver: &SymbolResolver) -> Option { + let mut ty = self.ty.clone(); + symbol_resolver.resolve(&mut ty, None); + + let syn::Type::Path(type_path) = ty else { + return None; + }; + + Some( + type_path + .path + .segments + .iter() + .map(|s| s.ident.to_string()) + .collect::>() + .join("::"), + ) + } + + fn is_option_type(&self, symbol_resolver: &SymbolResolver) -> bool { + matches!( + self.resolved_type_name(symbol_resolver).as_deref(), + Some("Option" | "std::option::Option" | "core::option::Option") + ) + } + + fn find_foreign_key_type(&self, symbol_resolver: &SymbolResolver) -> Option { + self.find_type("cot::db::ForeignKey", symbol_resolver) + .or_else(|| self.find_type("ForeignKey", symbol_resolver)) + } + /// Convert the field options into a field. /// /// # Panics @@ -214,12 +314,44 @@ impl FieldOpts { name.unraw().to_string() }; - let (auto_value, foreign_key) = ( - self.find_type("cot::db::Auto", symbol_resolver).is_some(), - self.find_type("cot::db::ForeignKey", symbol_resolver) - .map(ForeignKeySpec::try_from) - .transpose()?, - ); + let auto_value = self.find_type("cot::db::Auto", symbol_resolver).is_some(); + let foreign_key_ty = self.find_foreign_key_type(symbol_resolver); + + if self.foreign_key.is_some() && foreign_key_ty.is_none() { + return Err(syn::Error::new( + self.ident.span(), + "`#[model(foreign_key(...))]` can only be used on `ForeignKey` fields", + )); + } + + let foreign_key = foreign_key_ty + .map(ForeignKeySpec::try_from) + .transpose()? + .map(|mut fk| { + let args = self.foreign_key.unwrap_or_default(); + fk.set_on_delete(args.on_delete); + fk.set_on_update(args.on_update); + fk + }); + + // SetNone can only be used with Option types. + if let Some(foreign_key) = &foreign_key { + let uses_set_none = matches!( + foreign_key.on_delete, + Some(ForeignKeyOnDeletePolicy::SetNone) + ) || matches!( + foreign_key.on_update, + Some(ForeignKeyOnUpdatePolicy::SetNone) + ); + + if uses_set_none && !self.is_option_type(symbol_resolver) { + return Err(syn::Error::new( + self.ident.span(), + "`set_none` foreign key policy can only be used on `Option>` fields", + )); + } + } + let is_primary_key = self.primary_key.is_present(); let mut resolved_ty = self.ty.clone(); symbol_resolver.resolve(&mut resolved_ty, self_reference); @@ -273,6 +405,19 @@ pub struct Field { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ForeignKeySpec { pub to_model: syn::Type, + pub on_delete: Option, + pub on_update: Option, +} + +impl ForeignKeySpec { + pub fn set_on_delete(&mut self, on_delete: ForeignKeyOnDeletePolicy) -> &mut Self { + self.on_delete = Some(on_delete); + self + } + pub fn set_on_update(&mut self, on_update: ForeignKeyOnUpdatePolicy) -> &mut Self { + self.on_update = Some(on_update); + self + } } impl TryFrom for ForeignKeySpec { @@ -307,6 +452,8 @@ impl TryFrom for ForeignKeySpec { if let syn::GenericArgument::Type(ty) = inner { Ok(Self { to_model: ty.clone(), + on_delete: None, + on_update: None, }) } else { Err(syn::Error::new( @@ -324,6 +471,34 @@ mod tests { use super::*; use crate::symbol_resolver::{SymbolResolver, VisibleSymbol, VisibleSymbolKind}; + macro_rules! assert_foreign_key_policies { + ( + $test_name:ident, + $field_ty:ty, + on_delete = $on_delete:literal, + on_update = $on_update:literal, + expected_on_delete = $expected_on_delete:path, + expected_on_update = $expected_on_update:path + ) => { + #[test] + fn $test_name() { + let input: syn::Field = parse_quote! { + #[model(foreign_key(on_delete = $on_delete, on_update = $on_update))] + foo: $field_ty + }; + let field_opts = FieldOpts::from_field(&input).unwrap(); + let field = field_opts + .as_field(&SymbolResolver::new(vec![]), Some(&"Bar".to_string())) + .unwrap(); + + let foreign_key = field.foreign_key.unwrap(); + assert_eq!(foreign_key.to_model, parse_quote!(Foo)); + assert_eq!(foreign_key.on_delete, Some($expected_on_delete)); + assert_eq!(foreign_key.on_update, Some($expected_on_update)); + } + }; + } + #[test] fn model_args_default() { let args: ModelArgs = ModelArgs::default(); @@ -509,6 +684,126 @@ mod tests { assert_eq!(field.column_name, "test_field"); } + assert_foreign_key_policies!( + field_opts_foreign_key_restrict_restrict, + ForeignKey, + on_delete = "restrict", + on_update = "restrict", + expected_on_delete = ForeignKeyOnDeletePolicy::Restrict, + expected_on_update = ForeignKeyOnUpdatePolicy::Restrict + ); + + assert_foreign_key_policies!( + field_opts_foreign_key_cascade_cascade, + ForeignKey, + on_delete = "cascade", + on_update = "cascade", + expected_on_delete = ForeignKeyOnDeletePolicy::Cascade, + expected_on_update = ForeignKeyOnUpdatePolicy::Cascade + ); + + assert_foreign_key_policies!( + field_opts_foreign_key_no_action_restrict, + ForeignKey, + on_delete = "no_action", + on_update = "restrict", + expected_on_delete = ForeignKeyOnDeletePolicy::NoAction, + expected_on_update = ForeignKeyOnUpdatePolicy::Restrict + ); + + assert_foreign_key_policies!( + field_opts_option_foreign_key_set_none_set_none, + Option>, + on_delete = "set_none", + on_update = "set_none", + expected_on_delete = ForeignKeyOnDeletePolicy::SetNone, + expected_on_update = ForeignKeyOnUpdatePolicy::SetNone + ); + + #[test] + fn field_opts_foreign_key_on_delete_and_on_update_default() { + let input: syn::Field = parse_quote! { + foo: ForeignKey + }; + let field_opts = FieldOpts::from_field(&input).unwrap(); + let field = field_opts + .as_field(&SymbolResolver::new(vec![]), Some(&"Bar".to_string())) + .unwrap(); + + let foreign_key = field.foreign_key.unwrap(); + assert_eq!(foreign_key.to_model, parse_quote!(Foo)); + assert_eq!( + foreign_key.on_delete, + Some(ForeignKeyOnDeletePolicy::Restrict) + ); + assert_eq!( + foreign_key.on_update, + Some(ForeignKeyOnUpdatePolicy::Cascade) + ); + } + #[test] + fn field_opts_foreign_key_rejects_non_foreign_key_field() { + let input: syn::Field = parse_quote! { + #[model(foreign_key(on_delete = "cascade"))] + foo: i64 + }; + let field_opts = FieldOpts::from_field(&input).unwrap(); + let err = field_opts + .as_field(&SymbolResolver::new(vec![]), Some(&"Bar".to_string())) + .unwrap_err(); + + assert_eq!( + err.to_string(), + "`#[model(foreign_key(...))]` can only be used on `ForeignKey` fields" + ); + } + + #[test] + fn field_opts_foreign_key_rejects_set_none_on_required_foreign_key() { + let input: syn::Field = parse_quote! { + #[model(foreign_key(on_delete = "set_none"))] + foo: ForeignKey + }; + let field_opts = FieldOpts::from_field(&input).unwrap(); + let err = field_opts + .as_field(&SymbolResolver::new(vec![]), Some(&"Bar".to_string())) + .unwrap_err(); + + assert_eq!( + err.to_string(), + "`set_none` foreign key policy can only be used on `Option>` fields" + ); + } + + #[test] + fn field_opts_option_foreign_key_set_none_with_option_alias() { + let input: syn::Field = parse_quote! { + #[model(foreign_key(on_delete = "set_none", on_update = "set_none"))] + foo: Maybe> + }; + let field_opts = FieldOpts::from_field(&input).unwrap(); + let resolver = SymbolResolver::new(vec![VisibleSymbol::new( + "Maybe", + "std::option::Option", + VisibleSymbolKind::Use, + )]); + + let field = field_opts + .as_field(&resolver, Some(&"Bar".to_string())) + .unwrap(); + + let foreign_key = field.foreign_key.unwrap(); + assert_eq!(foreign_key.to_model, parse_quote!(Foo)); + assert_eq!( + foreign_key.on_delete, + Some(ForeignKeyOnDeletePolicy::SetNone) + ); + assert_eq!( + foreign_key.on_update, + Some(ForeignKeyOnUpdatePolicy::SetNone) + ); + } + #[test] fn find_type_resolved() { let input: syn::Type = @@ -534,6 +829,7 @@ mod tests { primary_key: darling::util::Flag::default(), unique: darling::util::Flag::default(), field_name: None, + foreign_key: None, }; assert!(opts.find_type("my_crate::MyContainer", &resolver).is_some()); diff --git a/cot-macros/src/lib.rs b/cot-macros/src/lib.rs index 1ca1b4790..085d4b7cb 100644 --- a/cot-macros/src/lib.rs +++ b/cot-macros/src/lib.rs @@ -44,155 +44,6 @@ pub fn derive_admin_model(input: TokenStream) -> TokenStream { token_stream.into() } -/// Implement the [`Model`] trait for a struct. -/// -/// This macro will generate an implementation of the [`Model`] trait for the -/// given named struct. Note that all the fields of the struct **must** -/// implement the [`DatabaseField`] trait. -/// -/// # Model Attributes -/// Cot provides a number of attributes that can be used on a struct to specify -/// how it should be treated by the migration engine and other parts of Cot. -/// These attributes are specified using the `#[model(...)]` attribute on the -/// struct. -/// -/// ## `model_type` -/// The model type can be specified using the `model_type` parameter. The model -/// type can be one of the following: -/// -/// * `application` (default): The model represents an actual table in a -/// normally running instance of the application. -/// ``` -/// use cot::db::{Auto, model}; -/// -/// #[model(model_type = "application")] -/// // This is equivalent to: -/// // #[model] -/// struct User { -/// #[model(primary_key)] -/// id: Auto, -/// username: String, -/// } -/// ``` -/// * `migration`: The model represents a table that is used for migrations. The -/// model name must be prefixed with an underscore. You shouldn't ever need to -/// use this type; the migration engine will generate the migration model -/// types for you. -/// -/// Migration models have two major uses. First, they ensure that the -/// migration engine knows what state the model was in at the time the last -/// migration was generated. This allows the engine to automatically detect -/// the changes and generate the necessary migration code. Second, they allow -/// custom code in migrations: you might want the migration to fill in some -/// data, for example. After the migration has been created, though, the model -/// might have changed. If you tried to use the application model in the -/// migration code (which always represents the latest state of the model), it -/// might not match the actual database schema at the time the migration is -/// applied. You can use the migration model to ensure that your custom code -/// operates exactly on the schema that is present at the time the migration -/// is applied. -/// -/// ``` -/// // In a migration file -/// use cot::db::{Auto, model}; -/// -/// #[model(model_type = "migration")] -/// struct _User { -/// #[model(primary_key)] -/// id: Auto, -/// username: String, -/// } -/// ``` -/// * `internal`: The model represents a table that is used internally by Cot -/// (e.g. the `cot__migrations` table, storing which migrations have been -/// applied). They are ignored by the migration generator and should never be -/// used outside Cot code. -/// ``` -/// use cot::db::{Auto, model}; -/// -/// #[model(model_type = "internal")] -/// struct CotMigrations { -/// #[model(primary_key)] -/// id: Auto, -/// app: String, -/// name: String, -/// } -/// ``` -/// -/// ## `table_name` -/// By default, the table name is the same as the struct name, converted to -/// snake case. You can specify a custom table name using the `table_name` -/// parameter: -/// -/// ``` -/// use cot::db::{Auto, model}; -/// -/// #[model(table_name = "users")] -/// struct User { -/// #[model(primary_key)] -/// id: Auto, -/// username: String, -/// } -/// ``` -/// -/// # Field Attributes -/// In addition to the struct-level attributes, you can also specify field-level -/// attributes using the `#[model(...)]` attribute, which is used to specify -/// field-level constraints and properties. -/// -/// ## `primary_key` -/// The `primary_key` attribute is used to specify that a field is the primary -/// key of the model. This attribute is required and must be used on exactly one -/// field of the struct. -/// -/// ``` -/// use cot::db::{Auto, model}; -/// -/// #[model] -/// struct User { -/// #[model(primary_key)] -/// id: Auto, -/// username: String, -/// } -/// ``` -/// -/// ## `unique` -/// The `unique` attribute is used to specify that a field must be unique across -/// all rows in the database. This will create a unique constraint on the -/// corresponding column in the database. -/// -/// ``` -/// use cot::db::{Auto, model}; -/// -/// #[model] -/// struct User { -/// #[model(primary_key)] -/// id: Auto, -/// #[model(unique)] -/// username: String, -/// } -/// ``` -/// -/// ## `field_name` -/// The `field_name` attribute is used to provide a specific name for the field -/// in the created database table to which the Rust field is mapped to. This -/// allows, in the following example, to map the `name` parameter to the -/// `username` column in the database. -/// -/// ``` -/// use cot::db::{Auto, model}; -/// -/// #[model] -/// struct User { -/// #[model(primary_key)] -/// id: Auto, -/// #[model(field_name = "username")] -/// name: String, -/// } -/// ``` -/// -/// [`Model`]: trait.Model.html -/// [`DatabaseField`]: trait.DatabaseField.html #[proc_macro_attribute] pub fn model(args: TokenStream, input: TokenStream) -> TokenStream { let attr_args = match NestedMeta::parse_meta_list(args.into()) { diff --git a/cot/src/db.rs b/cot/src/db.rs index 47a670933..d352f24a3 100644 --- a/cot/src/db.rs +++ b/cot/src/db.rs @@ -22,12 +22,253 @@ use std::sync::Arc; use async_trait::async_trait; use cot_core::error::impl_into_cot_error; +/// Implement the [`Model`] trait for a struct. +/// +/// This macro will generate an implementation of the [`Model`] trait for the +/// given named struct. Note that all the fields of the struct **must** +/// implement the [`DatabaseField`] trait. +/// +/// # Model Attributes +/// Cot provides a number of attributes that can be used on a struct to specify +/// how it should be treated by the migration engine and other parts of Cot. +/// These attributes are specified using the `#[model(...)]` attribute on the +/// struct. +/// +/// ## `model_type` +/// The model type can be specified using the `model_type` parameter. The model +/// type can be one of the following: +/// +/// * `application` (default): The model represents an actual table in a +/// normally running instance of the application. +/// ``` +/// use cot::db::{Auto, model}; +/// +/// #[model(model_type = "application")] +/// // This is equivalent to: +/// // #[model] +/// struct User { +/// #[model(primary_key)] +/// id: Auto, +/// username: String, +/// } +/// ``` +/// * `migration`: The model represents a table that is used for migrations. The +/// model name must be prefixed with an underscore. You shouldn't ever need to +/// use this type; the migration engine will generate the migration model +/// types for you. +/// +/// Migration models have two major uses. First, they ensure that the +/// migration engine knows what state the model was in at the time the last +/// migration was generated. This allows the engine to automatically detect +/// the changes and generate the necessary migration code. Second, they allow +/// custom code in migrations: you might want the migration to fill in some +/// data, for example. After the migration has been created, though, the model +/// might have changed. If you tried to use the application model in the +/// migration code (which always represents the latest state of the model), it +/// might not match the actual database schema at the time the migration is +/// applied. You can use the migration model to ensure that your custom code +/// operates exactly on the schema that is present at the time the migration +/// is applied. +/// +/// ``` +/// // In a migration file +/// use cot::db::{Auto, model}; +/// +/// #[model(model_type = "migration")] +/// struct _User { +/// #[model(primary_key)] +/// id: Auto, +/// username: String, +/// } +/// ``` +/// * `internal`: The model represents a table that is used internally by Cot +/// (e.g. the `cot__migrations` table, storing which migrations have been +/// applied). They are ignored by the migration generator and should never be +/// used outside Cot code. +/// ``` +/// use cot::db::{Auto, model}; +/// +/// #[model(model_type = "internal")] +/// struct CotMigrations { +/// #[model(primary_key)] +/// id: Auto, +/// app: String, +/// name: String, +/// } +/// ``` +/// +/// ## `table_name` +/// By default, the table name is the same as the struct name, converted to +/// snake case. You can specify a custom table name using the `table_name` +/// parameter: +/// +/// ``` +/// use cot::db::{Auto, model}; +/// +/// #[model(table_name = "users")] +/// struct User { +/// #[model(primary_key)] +/// id: Auto, +/// username: String, +/// } +/// ``` +/// +/// # Field Attributes +/// In addition to the struct-level attributes, you can also specify field-level +/// attributes using the `#[model(...)]` attribute, which is used to specify +/// field-level constraints and properties. +/// +/// ## `primary_key` +/// The `primary_key` attribute is used to specify that a field is the primary +/// key of the model. This attribute is required and must be used on exactly one +/// field of the struct. +/// +/// ``` +/// use cot::db::{Auto, model}; +/// +/// #[model] +/// struct User { +/// #[model(primary_key)] +/// id: Auto, +/// username: String, +/// } +/// ``` +/// +/// ## `unique` +/// The `unique` attribute is used to specify that a field must be unique across +/// all rows in the database. This will create a unique constraint on the +/// corresponding column in the database. +/// +/// ``` +/// use cot::db::{Auto, model}; +/// +/// #[model] +/// struct User { +/// #[model(primary_key)] +/// id: Auto, +/// #[model(unique)] +/// username: String, +/// } +/// ``` +/// +/// ## `field_name` +/// The `field_name` attribute is used to provide a specific name for the field +/// in the created database table to which the Rust field is mapped to. This +/// allows, in the following example, to map the `name` parameter to the +/// `username` column in the database. +/// +/// ``` +/// use cot::db::{Auto, model}; +/// +/// #[model] +/// struct User { +/// #[model(primary_key)] +/// id: Auto, +/// #[model(field_name = "username")] +/// name: String, +/// } +/// ``` +/// +/// ## `foreign_key` +/// +/// The `foreign_key` attribute configures the referential integrity behavior +/// of a [`ForeignKey`] field. It accepts two optional parameters: +/// +/// * `on_delete` — behavior when the referenced row is deleted. Defaults to +/// `"restrict"`. +/// * `on_update` — behavior when the referenced row is updated. Defaults to +/// `"cascade"`. +/// +/// > **Note:** The `foreign_key` attribute is only valid on [`ForeignKey`] +/// > fields. Using it +/// > on any other field type will result in a compile error. +/// +/// +/// ### Basic Usage +/// +/// ``` +/// use cot::db::{Auto, ForeignKey, model}; +/// +/// #[model] +/// struct User { +/// #[model(primary_key)] +/// id: Auto, +/// } +/// +/// #[model] +/// struct Post { +/// #[model(primary_key)] +/// id: Auto, +/// #[model(foreign_key(on_delete = "cascade", on_update = "cascade"))] +/// user_id: ForeignKey, +/// } +/// ``` +/// +/// ### Supported Policies +/// +/// Both `on_delete` and `on_update` accept the following values: +/// +/// * `cascade`: Automatically propagates the delete or update to all +/// referencing rows. +/// +/// * `restrict`: Prevents the delete or update of a referenced row if any +/// referencing rows exist. +/// +/// * `no_action`: Similar to `restrict`, but the referential integrity check is +/// deferred until the end of the transaction, depending on the database. +/// +/// * `set_none`: Sets the foreign key column to `NULL` when the referenced row +/// is deleted or updated. Requires the field to be of type +/// `Option>`. +/// +/// ### Examples +/// +/// **Cascade deletes, restrict updates** — deleting a `User` cascades to all +/// their `Post`s, but updating a `User`'s primary key is prevented if posts +/// exist: +/// +/// ``` +/// use cot::db::{Auto, ForeignKey, model}; +/// +/// # #[model] +/// # struct User { +/// # #[model(primary_key)] +/// # id: Auto, +/// # } +/// +/// #[model] +/// struct Post { +/// #[model(primary_key)] +/// id: Auto, +/// #[model(foreign_key(on_delete = "cascade", on_update = "restrict"))] +/// user_id: ForeignKey, +/// } +/// ``` +/// +/// **Set none on delete** — when a `User` is deleted, `author_id` is set to +/// `NULL` rather than deleting the `Post`. Note the required `Option` wrapper: +/// +/// ``` +/// use cot::db::{Auto, ForeignKey, model}; +/// # #[model] +/// # struct User { +/// # #[model(primary_key)] +/// # id: Auto, +/// # } +/// #[model] +/// struct Post { +/// #[model(primary_key)] +/// id: Auto, +/// #[model(foreign_key(on_delete = "set_none", on_update = "no_action"))] +/// author_id: Option>, +/// } +/// ``` pub use cot_macros::model; /// A convenient macro that allows you to write queries in a declarative /// fashion. /// /// `query!` parses a query expression and lowers it into a -/// [`Query`](cot::db::query::Query) builder. The resulting query is still lazy: +/// [`Query`] builder. The resulting query is still lazy: /// it is only executed when you call a terminal query method such as /// [`Query::get`](cot::db::query::Query::get) or /// [`Query::all`](cot::db::query::Query::all).