1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4 Attribute, Data, DeriveInput, Field, Fields, Ident, LitStr, Token, parse_macro_input,
5 punctuated::Punctuated,
6};
7
8mod relations;
9mod static_query;
10
11#[proc_macro]
69pub fn premix_query(input: TokenStream) -> TokenStream {
70 let input = parse_macro_input!(input as static_query::StaticQueryInput);
71 TokenStream::from(static_query::generate_static_query(input))
72}
73
74#[proc_macro_derive(Model, attributes(has_many, belongs_to, premix))]
75pub fn derive_model(input: TokenStream) -> TokenStream {
76 let input = parse_macro_input!(input as DeriveInput);
77 match derive_model_impl(&input) {
78 Ok(tokens) => TokenStream::from(tokens),
79 Err(err) => TokenStream::from(err.to_compile_error()),
80 }
81}
82
83fn derive_model_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
84 let impl_block = generate_generic_impl(input)?;
85 let rel_block = relations::impl_relations(input)?;
86 Ok(quote! {
87 #impl_block
88 #rel_block
89 })
90}
91
92fn generate_generic_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
93 let struct_name = &input.ident;
94 let table_name = struct_name.to_string().to_lowercase() + "s";
95 let custom_hooks = has_premix_flag(&input.attrs, "custom_hooks");
96 let custom_validation = has_premix_flag(&input.attrs, "custom_validation");
97
98 let all_fields = if let Data::Struct(data) = &input.data {
99 if let Fields::Named(fields) = &data.fields {
100 &fields.named
101 } else {
102 return Err(syn::Error::new_spanned(
103 &data.fields,
104 "Premix Model only supports structs with named fields",
105 ));
106 }
107 } else {
108 return Err(syn::Error::new_spanned(
109 input,
110 "Premix Model only supports structs",
111 ));
112 };
113
114 let mut db_fields = Vec::new();
115 let mut ignored_field_idents = Vec::new();
116
117 for field in all_fields {
118 if is_ignored(field) {
119 ignored_field_idents.push(field.ident.as_ref().unwrap());
120 } else {
121 db_fields.push(field);
122 }
123 }
124
125 let field_idents: Vec<_> = db_fields
126 .iter()
127 .map(|f| f.ident.as_ref().unwrap())
128 .collect();
129 let field_types: Vec<_> = db_fields.iter().map(|f| &f.ty).collect();
130 let _field_indices: Vec<_> = (0..db_fields.len()).collect();
131 let field_names: Vec<_> = field_idents.iter().map(|id| id.to_string()).collect();
132 let field_names_no_id: Vec<_> = field_names
133 .iter()
134 .filter(|name| *name != "id")
135 .cloned()
136 .collect();
137 let field_names_no_id_len = field_names_no_id.len();
138 let all_cols_head = field_names.first().cloned().unwrap_or_default();
142 let all_cols_tail: Vec<_> = field_names.iter().skip(1).cloned().collect();
143
144 let no_id_cols_head = field_names_no_id.first().cloned().unwrap_or_default();
145 let no_id_cols_tail: Vec<_> = field_names_no_id.iter().skip(1).cloned().collect();
146
147 let field_idents_len = field_idents.len();
148 let field_nullables: Vec<_> = db_fields.iter().map(|f| is_option_type(&f.ty)).collect();
149 let field_primary_keys: Vec<_> = field_names.iter().map(|n| n == "id").collect();
150 let field_sql_types: Vec<_> = db_fields
151 .iter()
152 .map(|field| {
153 let name = field.ident.as_ref().unwrap().to_string();
154 sql_type_for_field(&name, &field.ty).to_string()
155 })
156 .collect();
157 let field_sql_type_exprs: Vec<_> = db_fields
158 .iter()
159 .map(|field| {
160 let name = field.ident.as_ref().unwrap().to_string();
161 sql_type_expr_for_field(&name, &field.ty)
162 })
163 .collect();
164 let sensitive_field_literals: Vec<LitStr> = db_fields
165 .iter()
166 .filter(|f| is_sensitive(f))
167 .map(|f| {
168 LitStr::new(
169 &f.ident.as_ref().unwrap().to_string(),
170 f.ident.as_ref().unwrap().span(),
171 )
172 })
173 .collect();
174
175 let eager_load_body = relations::generate_eager_load_body(input)?;
176 let (index_specs, foreign_key_specs) = collect_schema_specs(all_fields, &table_name)?;
177 let index_tokens: Vec<_> = index_specs
178 .iter()
179 .map(|spec| {
180 let name = &spec.name;
181 let columns = &spec.columns;
182 let unique = spec.unique;
183 quote! {
184 premix_orm::schema::SchemaIndex {
185 name: #name.to_string(),
186 columns: vec![#(#columns.to_string()),*],
187 unique: #unique,
188 }
189 }
190 })
191 .collect();
192 let foreign_key_tokens: Vec<_> = foreign_key_specs
193 .iter()
194 .map(|spec| {
195 let column = &spec.column;
196 let ref_table = &spec.ref_table;
197 let ref_column = &spec.ref_column;
198 quote! {
199 premix_orm::schema::SchemaForeignKey {
200 column: #column.to_string(),
201 ref_table: #ref_table.to_string(),
202 ref_column: #ref_column.to_string(),
203 }
204 }
205 })
206 .collect();
207 let has_version = field_names.contains(&"version".to_string());
208 let has_soft_delete = field_names.contains(&"deleted_at".to_string());
209
210 let update_impl = if has_version {
211 quote! {
212 fn update<'a, E>(
213 &'a mut self,
214 executor: E,
215 ) -> impl ::std::future::Future<
216 Output = Result<premix_orm::UpdateResult, premix_orm::sqlx::Error>,
217 > + Send
218 where
219 E: premix_orm::IntoExecutor<'a, DB = DB>
220 {
221 async move {
222 let mut executor = executor.into_executor();
223 let table_name = Self::table_name();
224 static SQL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
225 let sql = SQL.get_or_init(|| {
226 let mut set_clause = String::with_capacity(#field_idents_len * 8);
227 let mut i = 1usize;
228 #(
229 if i > 1 {
230 set_clause.push_str(", ");
231 }
232 set_clause.push_str(#field_names);
233 set_clause.push_str(" = ");
234 set_clause.push_str(&<DB as premix_orm::SqlDialect>::placeholder(i));
235 i += 1;
236 )*
237 let id_p = <DB as premix_orm::SqlDialect>::placeholder(1 + #field_idents_len);
238 let ver_p = <DB as premix_orm::SqlDialect>::placeholder(2 + #field_idents_len);
239 let mut sql = String::with_capacity(set_clause.len() + table_name.len() + 64);
240 use ::std::fmt::Write;
241 let _ = write!(
242 sql,
243 "UPDATE {} SET {}, version = version + 1 WHERE id = {} AND version = {}",
244 table_name,
245 set_clause,
246 id_p,
247 ver_p
248 );
249 sql
250 });
251
252 premix_orm::tracing::debug!(
253 operation = "update",
254 table = table_name,
255 sql = %sql,
256 "premix query"
257 );
258
259 let mut query = premix_orm::sqlx::query::<DB>(sql).persistent(true)
260 #( .bind(&self.#field_idents) )*
261 .bind(&self.id)
262 .bind(&self.version);
263
264 let result = executor.execute(query).await?;
265
266 if <DB as premix_orm::SqlDialect>::rows_affected(&result) == 0 {
267 static EXISTS_SQL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
268 let exists_sql = EXISTS_SQL.get_or_init(|| {
269 let exists_p = <DB as premix_orm::SqlDialect>::placeholder(1);
270 let mut exists_sql = String::with_capacity(table_name.len() + 32);
271 use ::std::fmt::Write;
272 let _ = write!(exists_sql, "SELECT id FROM {} WHERE id = {}", table_name, exists_p);
273 exists_sql
274 });
275 let exists_query =
276 premix_orm::sqlx::query_as::<DB, (i32,)>(exists_sql)
277 .persistent(true)
278 .bind(&self.id);
279 let exists = executor.fetch_optional(exists_query).await?;
280
281 if exists.is_none() {
282 Ok(premix_orm::UpdateResult::NotFound)
283 } else {
284 Ok(premix_orm::UpdateResult::VersionConflict)
285 }
286 } else {
287 self.version += 1;
288 Ok(premix_orm::UpdateResult::Success)
289 }
290 }
291 }
292
293 fn update_fast<'a, E>(
294 &'a mut self,
295 executor: E,
296 ) -> impl ::std::future::Future<
297 Output = Result<premix_orm::UpdateResult, premix_orm::sqlx::Error>,
298 > + Send
299 where
300 E: premix_orm::IntoExecutor<'a, DB = DB>
301 {
302 async move {
303 let mut executor = executor.into_executor();
304 let table_name = Self::table_name();
305 static SQL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
306 let sql = SQL.get_or_init(|| {
307 let mut set_clause = String::with_capacity(#field_idents_len * 8);
308 let mut i = 1usize;
309 #(
310 if i > 1 {
311 set_clause.push_str(", ");
312 }
313 set_clause.push_str(#field_names);
314 set_clause.push_str(" = ");
315 set_clause.push_str(&<DB as premix_orm::SqlDialect>::placeholder(i));
316 i += 1;
317 )*
318 let id_p = <DB as premix_orm::SqlDialect>::placeholder(1 + #field_idents_len);
319 let ver_p = <DB as premix_orm::SqlDialect>::placeholder(2 + #field_idents_len);
320 let mut sql = String::with_capacity(set_clause.len() + table_name.len() + 64);
321 use ::std::fmt::Write;
322 let _ = write!(
323 sql,
324 "UPDATE {} SET {}, version = version + 1 WHERE id = {} AND version = {}",
325 table_name,
326 set_clause,
327 id_p,
328 ver_p
329 );
330 sql
331 });
332
333 let mut query = premix_orm::sqlx::query::<DB>(sql).persistent(true)
334 #( .bind(&self.#field_idents) )*
335 .bind(&self.id)
336 .bind(&self.version);
337
338 let result = executor.execute(query).await?;
339
340 if <DB as premix_orm::SqlDialect>::rows_affected(&result) == 0 {
341 static EXISTS_SQL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
342 let exists_sql = EXISTS_SQL.get_or_init(|| {
343 let exists_p = <DB as premix_orm::SqlDialect>::placeholder(1);
344 let mut exists_sql = String::with_capacity(table_name.len() + 32);
345 use ::std::fmt::Write;
346 let _ = write!(exists_sql, "SELECT id FROM {} WHERE id = {}", table_name, exists_p);
347 exists_sql
348 });
349 let exists_query =
350 premix_orm::sqlx::query_as::<DB, (i32,)>(exists_sql)
351 .persistent(true)
352 .bind(&self.id);
353 let exists = executor.fetch_optional(exists_query).await?;
354
355 if exists.is_none() {
356 Ok(premix_orm::UpdateResult::NotFound)
357 } else {
358 Ok(premix_orm::UpdateResult::VersionConflict)
359 }
360 } else {
361 self.version += 1;
362 Ok(premix_orm::UpdateResult::Success)
363 }
364 }
365 }
366 fn update_ultra<'a, E>(
367 &'a mut self,
368 executor: E,
369 ) -> impl ::std::future::Future<
370 Output = Result<premix_orm::UpdateResult, premix_orm::sqlx::Error>,
371 > + Send
372 where
373 E: premix_orm::IntoExecutor<'a, DB = DB>
374 {
375 async move { self.update_fast(executor).await }
376 }
377 }
378 } else {
379 quote! {
380 fn update<'a, E>(
381 &'a mut self,
382 executor: E,
383 ) -> impl ::std::future::Future<
384 Output = Result<premix_orm::UpdateResult, premix_orm::sqlx::Error>,
385 > + Send
386 where
387 E: premix_orm::IntoExecutor<'a, DB = DB>
388 {
389 async move {
390 let mut executor = executor.into_executor();
391 let table_name = Self::table_name();
392 static SQL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
393 let sql = SQL.get_or_init(|| {
394 let mut set_clause = String::with_capacity(#field_idents_len * 8);
395 let mut i = 1usize;
396 #(
397 if i > 1 {
398 set_clause.push_str(", ");
399 }
400 set_clause.push_str(#field_names);
401 set_clause.push_str(" = ");
402 set_clause.push_str(&<DB as premix_orm::SqlDialect>::placeholder(i));
403 i += 1;
404 )*
405 let id_p = <DB as premix_orm::SqlDialect>::placeholder(1 + #field_idents_len);
406 let mut sql = String::with_capacity(set_clause.len() + table_name.len() + 32);
407 use ::std::fmt::Write;
408 let _ = write!(sql, "UPDATE {} SET {} WHERE id = {}", table_name, set_clause, id_p);
409 sql
410 });
411
412 premix_orm::tracing::debug!(
413 operation = "update",
414 table = table_name,
415 sql = %sql,
416 "premix query"
417 );
418
419 let mut query = premix_orm::sqlx::query::<DB>(sql).persistent(true)
420 #( .bind(&self.#field_idents) )*
421 .bind(&self.id);
422
423 let result = executor.execute(query).await?;
424
425 if <DB as premix_orm::SqlDialect>::rows_affected(&result) == 0 {
426 Ok(premix_orm::UpdateResult::NotFound)
427 } else {
428 Ok(premix_orm::UpdateResult::Success)
429 }
430 }
431 }
432
433 fn update_fast<'a, E>(
434 &'a mut self,
435 executor: E,
436 ) -> impl ::std::future::Future<
437 Output = Result<premix_orm::UpdateResult, premix_orm::sqlx::Error>,
438 > + Send
439 where
440 E: premix_orm::IntoExecutor<'a, DB = DB>
441 {
442 async move {
443 let mut executor = executor.into_executor();
444 let table_name = Self::table_name();
445 static SQL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
446 let sql = SQL.get_or_init(|| {
447 let mut set_clause = String::with_capacity(#field_idents_len * 8);
448 let mut i = 1usize;
449 #(
450 if i > 1 {
451 set_clause.push_str(", ");
452 }
453 set_clause.push_str(#field_names);
454 set_clause.push_str(" = ");
455 set_clause.push_str(&<DB as premix_orm::SqlDialect>::placeholder(i));
456 i += 1;
457 )*
458 let id_p = <DB as premix_orm::SqlDialect>::placeholder(1 + #field_idents_len);
459 let mut sql = String::with_capacity(set_clause.len() + table_name.len() + 32);
460 use ::std::fmt::Write;
461 let _ = write!(sql, "UPDATE {} SET {} WHERE id = {}", table_name, set_clause, id_p);
462 sql
463 });
464
465 let mut query = premix_orm::sqlx::query::<DB>(sql).persistent(true)
466 #( .bind(&self.#field_idents) )*
467 .bind(&self.id);
468
469 let result = executor.execute(query).await?;
470
471 if <DB as premix_orm::SqlDialect>::rows_affected(&result) == 0 {
472 Ok(premix_orm::UpdateResult::NotFound)
473 } else {
474 Ok(premix_orm::UpdateResult::Success)
475 }
476 }
477 }
478 fn update_ultra<'a, E>(
479 &'a mut self,
480 executor: E,
481 ) -> impl ::std::future::Future<
482 Output = Result<premix_orm::UpdateResult, premix_orm::sqlx::Error>,
483 > + Send
484 where
485 E: premix_orm::IntoExecutor<'a, DB = DB>
486 {
487 async move { self.update_fast(executor).await }
488 }
489 }
490 };
491
492 let delete_impl = if has_soft_delete {
493 quote! {
494 fn delete<'a, E>(
495 &'a mut self,
496 executor: E,
497 ) -> impl ::std::future::Future<Output = Result<(), premix_orm::sqlx::Error>>
498 + Send
499 where
500 E: premix_orm::IntoExecutor<'a, DB = DB>
501 {
502 async move {
503 let mut executor = executor.into_executor();
504 let table_name = Self::table_name();
505 static SQL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
506 let sql = SQL.get_or_init(|| {
507 let id_p = <DB as premix_orm::SqlDialect>::placeholder(1);
508 let mut sql = String::with_capacity(table_name.len() + 64);
509 use ::std::fmt::Write;
510 let _ = write!(
511 sql,
512 "UPDATE {} SET deleted_at = {} WHERE id = {}",
513 table_name,
514 <DB as premix_orm::SqlDialect>::current_timestamp_fn(),
515 id_p
516 );
517 sql
518 });
519
520 premix_orm::tracing::debug!(
521 operation = "delete",
522 table = table_name,
523 sql = %sql,
524 "premix query"
525 );
526
527 let query = premix_orm::sqlx::query::<DB>(sql).persistent(true).bind(&self.id);
528 executor.execute(query).await?;
529
530 self.deleted_at = Some("DELETED".to_string());
531 Ok(())
532 }
533 }
534 fn delete_fast<'a, E>(
535 &'a mut self,
536 executor: E,
537 ) -> impl ::std::future::Future<Output = Result<(), premix_orm::sqlx::Error>>
538 + Send
539 where
540 E: premix_orm::IntoExecutor<'a, DB = DB>
541 {
542 async move {
543 let mut executor = executor.into_executor();
544 let table_name = Self::table_name();
545 static SQL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
546 let sql = SQL.get_or_init(|| {
547 let id_p = <DB as premix_orm::SqlDialect>::placeholder(1);
548 let mut sql = String::with_capacity(table_name.len() + 64);
549 use ::std::fmt::Write;
550 let _ = write!(
551 sql,
552 "UPDATE {} SET deleted_at = {} WHERE id = {}",
553 table_name,
554 <DB as premix_orm::SqlDialect>::current_timestamp_fn(),
555 id_p
556 );
557 sql
558 });
559
560 let query = premix_orm::sqlx::query::<DB>(sql).persistent(true).bind(&self.id);
561 executor.execute(query).await?;
562
563 self.deleted_at = Some("DELETED".to_string());
564 Ok(())
565 }
566 }
567 fn delete_ultra<'a, E>(
568 &'a mut self,
569 executor: E,
570 ) -> impl ::std::future::Future<Output = Result<(), premix_orm::sqlx::Error>>
571 + Send
572 where
573 E: premix_orm::IntoExecutor<'a, DB = DB>
574 {
575 async move { self.delete_fast(executor).await }
576 }
577 fn has_soft_delete() -> bool { true }
578 }
579 } else {
580 quote! {
581 fn delete<'a, E>(
582 &'a mut self,
583 executor: E,
584 ) -> impl ::std::future::Future<Output = Result<(), premix_orm::sqlx::Error>>
585 + Send
586 where
587 E: premix_orm::IntoExecutor<'a, DB = DB>
588 {
589 async move {
590 let mut executor = executor.into_executor();
591 let table_name = Self::table_name();
592 static SQL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
593 let sql = SQL.get_or_init(|| {
594 let id_p = <DB as premix_orm::SqlDialect>::placeholder(1);
595 let mut sql = String::with_capacity(table_name.len() + 24);
596 use ::std::fmt::Write;
597 let _ = write!(sql, "DELETE FROM {} WHERE id = {}", table_name, id_p);
598 sql
599 });
600
601 premix_orm::tracing::debug!(
602 operation = "delete",
603 table = table_name,
604 sql = %sql,
605 "premix query"
606 );
607
608 let query = premix_orm::sqlx::query::<DB>(sql).persistent(true).bind(&self.id);
609 executor.execute(query).await?;
610
611 Ok(())
612 }
613 }
614 fn delete_fast<'a, E>(
615 &'a mut self,
616 executor: E,
617 ) -> impl ::std::future::Future<Output = Result<(), premix_orm::sqlx::Error>>
618 + Send
619 where
620 E: premix_orm::IntoExecutor<'a, DB = DB>
621 {
622 async move {
623 let mut executor = executor.into_executor();
624 let table_name = Self::table_name();
625 static SQL: ::std::sync::OnceLock<String> = ::std::sync::OnceLock::new();
626 let sql = SQL.get_or_init(|| {
627 let id_p = <DB as premix_orm::SqlDialect>::placeholder(1);
628 let mut sql = String::with_capacity(table_name.len() + 24);
629 use ::std::fmt::Write;
630 let _ = write!(sql, "DELETE FROM {} WHERE id = {}", table_name, id_p);
631 sql
632 });
633
634 let query = premix_orm::sqlx::query::<DB>(sql).persistent(true).bind(&self.id);
635 executor.execute(query).await?;
636
637 Ok(())
638 }
639 }
640 fn delete_ultra<'a, E>(
641 &'a mut self,
642 executor: E,
643 ) -> impl ::std::future::Future<Output = Result<(), premix_orm::sqlx::Error>>
644 + Send
645 where
646 E: premix_orm::IntoExecutor<'a, DB = DB>
647 {
648 async move { self.delete_fast(executor).await }
649 }
650 fn has_soft_delete() -> bool { false }
651 }
652 };
653
654 let mut related_model_bounds = Vec::new();
655 for field in all_fields {
656 for attr in &field.attrs {
657 if attr.path().is_ident("has_many")
658 && let Ok(related_ident) = attr.parse_args::<syn::Ident>()
659 {
660 related_model_bounds.push(quote! { #related_ident: premix_orm::Model<DB> });
661 } else if attr.path().is_ident("belongs_to")
662 && let Ok(related_ident) = attr.parse_args::<syn::Ident>()
663 {
664 related_model_bounds.push(quote! { #related_ident: premix_orm::Model<DB> + Clone });
665 }
666 }
667 }
668
669 let hooks_impl = if custom_hooks {
670 quote! {}
671 } else {
672 quote! {
673 impl premix_orm::ModelHooks for #struct_name {}
674 }
675 };
676
677 let validation_impl = if custom_validation {
678 quote! {}
679 } else {
680 quote! {
681 impl premix_orm::ModelValidation for #struct_name {}
682 }
683 };
684
685 let col_consts: Vec<_> = field_names
686 .iter()
687 .zip(field_idents.iter())
688 .map(|(name, ident)| {
689 let const_name = syn::Ident::new(&ident.to_string().to_uppercase(), ident.span());
690 quote! {
691 pub const #const_name: &str = #name;
692 }
693 })
694 .collect();
695
696 let columns_mod_ident = syn::Ident::new(
697 &format!("columns_{}", struct_name.to_string().to_lowercase()),
698 struct_name.span(),
699 );
700
701 Ok(quote! {
703 #[allow(non_snake_case)]
705 pub mod #columns_mod_ident {
706 #( #col_consts )*
707 }
708
709 impl<'r, R> premix_orm::sqlx::FromRow<'r, R> for #struct_name
710 where
711 R: premix_orm::sqlx::Row,
712 R::Database: premix_orm::sqlx::Database,
713 #(
714 #field_types: premix_orm::sqlx::Type<R::Database> + premix_orm::sqlx::Decode<'r, R::Database>,
715 )*
716 for<'c> &'c str: premix_orm::sqlx::ColumnIndex<R>,
717 {
718 fn from_row(row: &'r R) -> Result<Self, premix_orm::sqlx::Error> {
719 use premix_orm::sqlx::Row;
720 Ok(Self {
721 #(
722 #field_idents: row.try_get(#field_names)?,
723 )*
724 #(
725 #ignored_field_idents: None,
726 )*
727 })
728 }
729 }
730
731
732 impl<DB> premix_orm::Model<DB> for #struct_name
733 where
734 DB: premix_orm::SqlDialect,
735 for<'c> &'c str: premix_orm::sqlx::ColumnIndex<DB::Row>,
736 usize: premix_orm::sqlx::ColumnIndex<DB::Row>,
737 for<'q> <DB as premix_orm::sqlx::Database>::Arguments<'q>: premix_orm::sqlx::IntoArguments<'q, DB>,
738 for<'c> &'c mut <DB as premix_orm::sqlx::Database>::Connection: premix_orm::sqlx::Executor<'c, Database = DB>,
739 i32: premix_orm::sqlx::Type<DB> + for<'q> premix_orm::sqlx::Encode<'q, DB> + for<'r> premix_orm::sqlx::Decode<'r, DB>,
740 i64: premix_orm::sqlx::Type<DB> + for<'q> premix_orm::sqlx::Encode<'q, DB> + for<'r> premix_orm::sqlx::Decode<'r, DB>,
741 String: premix_orm::sqlx::Type<DB> + for<'q> premix_orm::sqlx::Encode<'q, DB> + for<'r> premix_orm::sqlx::Decode<'r, DB>,
742 bool: premix_orm::sqlx::Type<DB> + for<'q> premix_orm::sqlx::Encode<'q, DB> + for<'r> premix_orm::sqlx::Decode<'r, DB>,
743 Option<String>: premix_orm::sqlx::Type<DB> + for<'q> premix_orm::sqlx::Encode<'q, DB> + for<'r> premix_orm::sqlx::Decode<'r, DB>,
744 #( #related_model_bounds, )*
745 {
746 fn table_name() -> &'static str {
747 #table_name
748 }
749
750 fn create_table_sql() -> String {
751 let mut cols = vec!["id ".to_string() + <DB as premix_orm::SqlDialect>::auto_increment_pk()];
752 #(
753 if #field_names != "id" {
754 let sql_type = #field_sql_type_exprs;
755 cols.push(format!("{} {}", #field_names, sql_type));
756 }
757 )*
758 format!("CREATE TABLE IF NOT EXISTS {} ({})", #table_name, cols.join(", "))
759 }
760
761 fn list_columns() -> ::std::vec::Vec<::std::string::String> {
762 vec![ #( #field_names.to_string() ),* ]
763 }
764
765 fn sensitive_fields() -> &'static [&'static str] {
766 &[ #( #sensitive_field_literals ),* ]
767 }
768
769 fn from_row_fast(row: &<DB as premix_orm::sqlx::Database>::Row) -> Result<Self, premix_orm::sqlx::Error>
770 where
771 usize: premix_orm::sqlx::ColumnIndex<<DB as premix_orm::sqlx::Database>::Row>,
772 for<'c> &'c str: premix_orm::sqlx::ColumnIndex<<DB as premix_orm::sqlx::Database>::Row>,
773 {
774 use premix_orm::sqlx::Row;
775 let mut idx: usize = 0;
776 #(
777 let #field_idents = row.try_get(idx)?;
778 idx += 1;
779 )*
780 Ok(Self {
781 #( #field_idents, )*
782 #( #ignored_field_idents: None, )*
783 })
784 }
785
786 fn save<'a, E>(
787 &'a mut self,
788 executor: E,
789 ) -> impl ::std::future::Future<Output = ::std::result::Result<(), premix_orm::sqlx::Error>>
790 + Send
791 where
792 E: premix_orm::IntoExecutor<'a, DB = DB>
793 {
794 async move {
795 let mut executor = executor.into_executor();
796 use premix_orm::ModelHooks;
797 self.before_save().await?;
798
799 const ALL_COLUMNS_LIST: &str = concat!(#all_cols_head, #( ", ", #all_cols_tail ),*);
802 const NO_ID_COLUMNS_LIST: &str = concat!(#no_id_cols_head, #( ", ", #no_id_cols_tail ),*);
803
804 let column_list: &str = if self.id == 0 { NO_ID_COLUMNS_LIST } else { ALL_COLUMNS_LIST };
805
806 let count = if self.id == 0 { #field_names_no_id_len } else { #field_idents_len };
808 let placeholders = premix_orm::cached_placeholders::<DB>(count);
809
810 let supports_returning = <DB as premix_orm::SqlDialect>::supports_returning();
811 if supports_returning {
812 let sql = format!(
814 "INSERT INTO {} ({}) VALUES ({}) RETURNING id",
815 #table_name,
816 column_list,
817 placeholders
818 );
819
820 premix_orm::tracing::debug!(
821 operation = "insert",
822 table = #table_name,
823 sql = %sql,
824 "premix query"
825 );
826
827 let mut query = premix_orm::sqlx::query_as::<DB, (i32,)>(&sql)
828 .persistent(true);
829 #(
830 if #field_names != "id" {
831 query = query.bind(&self.#field_idents);
832 } else if self.id != 0 {
833 query = query.bind(&self.id);
834 }
835 )*
836
837 if let Some((id,)) = executor.fetch_optional(query).await? {
838 self.id = id;
839 }
840 } else {
841 let sql = format!(
842 "INSERT INTO {} ({}) VALUES ({})",
843 #table_name,
844 column_list,
845 placeholders
846 );
847
848 premix_orm::tracing::debug!(
849 operation = "insert",
850 table = #table_name,
851 sql = %sql,
852 "premix query"
853 );
854
855 let mut query = premix_orm::sqlx::query::<DB>(&sql).persistent(true);
856 #(
857 if #field_names != "id" {
858 query = query.bind(&self.#field_idents);
859 } else if self.id != 0 {
860 query = query.bind(&self.id);
861 }
862 )*
863
864 let result = executor.execute(query).await?;
865 let last_id = <DB as premix_orm::SqlDialect>::last_insert_id(&result);
866 if last_id > 0 {
867 self.id = last_id as i32;
868 }
869 }
870
871 self.after_save().await?;
872 Ok(())
873 }
874 }
875
876 fn save_fast<'a, E>(
877 &'a mut self,
878 executor: E,
879 ) -> impl ::std::future::Future<Output = ::std::result::Result<(), premix_orm::sqlx::Error>>
880 + Send
881 where
882 E: premix_orm::IntoExecutor<'a, DB = DB>
883 {
884 async move {
885 let mut executor = executor.into_executor();
886
887 const ALL_COLUMNS_LIST: &str = concat!(#all_cols_head, #( ", ", #all_cols_tail ),*);
890 const NO_ID_COLUMNS_LIST: &str = concat!(#no_id_cols_head, #( ", ", #no_id_cols_tail ),*);
891
892 let column_list: &str = if self.id == 0 { NO_ID_COLUMNS_LIST } else { ALL_COLUMNS_LIST };
893
894 let count = if self.id == 0 { #field_names_no_id_len } else { #field_idents_len };
896 let placeholders = premix_orm::cached_placeholders::<DB>(count);
897
898 let supports_returning = <DB as premix_orm::SqlDialect>::supports_returning();
899 if supports_returning {
900 let sql = format!(
901 "INSERT INTO {} ({}) VALUES ({}) RETURNING id",
902 #table_name,
903 column_list,
904 placeholders
905 );
906
907 let mut query = premix_orm::sqlx::query_as::<DB, (i32,)>(&sql)
908 .persistent(true);
909 #(
910 if #field_names != "id" {
911 query = query.bind(&self.#field_idents);
912 } else if self.id != 0 {
913 query = query.bind(&self.id);
914 }
915 )*
916
917 if let Some((id,)) = executor.fetch_optional(query).await? {
918 self.id = id;
919 }
920 } else {
921 let sql = format!(
922 "INSERT INTO {} ({}) VALUES ({})",
923 #table_name,
924 column_list,
925 placeholders
926 );
927
928 let mut query = premix_orm::sqlx::query::<DB>(&sql).persistent(true);
929 #(
930 if #field_names != "id" {
931 query = query.bind(&self.#field_idents);
932 } else if self.id != 0 {
933 query = query.bind(&self.id);
934 }
935 )*
936
937 let result = executor.execute(query).await?;
938 let last_id = <DB as premix_orm::SqlDialect>::last_insert_id(&result);
939 if last_id > 0 {
940 self.id = last_id as i32;
941 }
942 }
943
944 Ok(())
945 }
946 }
947 fn save_ultra<'a, E>(
948 &'a mut self,
949 executor: E,
950 ) -> impl ::std::future::Future<Output = ::std::result::Result<(), premix_orm::sqlx::Error>>
951 + Send
952 where
953 E: premix_orm::IntoExecutor<'a, DB = DB>
954 {
955 async move {
956 let mut executor = executor.into_executor();
957
958 const ALL_COLUMNS_LIST: &str = concat!(#all_cols_head, #( ", ", #all_cols_tail ),*);
961 const NO_ID_COLUMNS_LIST: &str = concat!(#no_id_cols_head, #( ", ", #no_id_cols_tail ),*);
962
963 let column_list: &str = if self.id == 0 { NO_ID_COLUMNS_LIST } else { ALL_COLUMNS_LIST };
964
965 let count = if self.id == 0 { #field_names_no_id_len } else { #field_idents_len };
967 let placeholders = premix_orm::cached_placeholders::<DB>(count);
968
969 let sql = format!(
970 "INSERT INTO {} ({}) VALUES ({})",
971 #table_name,
972 column_list,
973 placeholders
974 );
975
976 let mut query = premix_orm::sqlx::query::<DB>(&sql).persistent(true);
977 #(
978 if #field_names != "id" {
979 query = query.bind(&self.#field_idents);
980 } else if self.id != 0 {
981 query = query.bind(&self.id);
982 }
983 )*
984
985 let result = executor.execute(query).await?;
986 let last_id = <DB as premix_orm::SqlDialect>::last_insert_id(&result);
987 if last_id > 0 {
988 self.id = last_id as i32;
989 }
990
991 Ok(())
992 }
993 }
994
995 #update_impl
996 #delete_impl
997
998 fn find_by_id<'a, E>(
999 executor: E,
1000 id: i32,
1001 ) -> impl ::std::future::Future<Output = ::std::result::Result<::std::option::Option<Self>, premix_orm::sqlx::Error>>
1002 + Send
1003 where
1004 E: premix_orm::IntoExecutor<'a, DB = DB>
1005 {
1006 async move {
1007 let mut executor = executor.into_executor();
1008 let p = <DB as premix_orm::SqlDialect>::placeholder(1);
1009
1010 let sql = if Self::has_soft_delete() {
1012 format!("SELECT * FROM {} WHERE id = {} AND deleted_at IS NULL LIMIT 1", #table_name, p)
1013 } else {
1014 format!("SELECT * FROM {} WHERE id = {} LIMIT 1", #table_name, p)
1015 };
1016
1017 premix_orm::tracing::debug!(
1018 operation = "select",
1019 table = #table_name,
1020 sql = %sql,
1021 "premix query"
1022 );
1023 let query = premix_orm::sqlx::query_as::<DB, Self>(&sql)
1024 .persistent(true)
1025 .bind(id);
1026 executor.fetch_optional(query).await
1027 }
1028 }
1029
1030 fn eager_load<'a>(
1031 models: &mut [Self],
1032 relation: &str,
1033 executor: premix_orm::Executor<'a, DB>,
1034 ) -> impl ::std::future::Future<Output = Result<(), premix_orm::sqlx::Error>> + Send
1035 {
1036 async move {
1037 let mut executor = executor;
1038 #eager_load_body
1039 }
1040 }
1041 }
1042
1043 #hooks_impl
1044 #validation_impl
1045
1046 impl premix_orm::ModelSchema for #struct_name {
1047 fn schema() -> premix_orm::schema::SchemaTable {
1048 let columns = vec![
1049 #(
1050 premix_orm::schema::SchemaColumn {
1051 name: #field_names.to_string(),
1052 sql_type: #field_sql_types.to_string(),
1053 nullable: #field_nullables,
1054 primary_key: #field_primary_keys,
1055 }
1056 ),*
1057 ];
1058 let indexes = vec![
1059 #(#index_tokens),*
1060 ];
1061 let foreign_keys = vec![
1062 #(#foreign_key_tokens),*
1063 ];
1064 premix_orm::schema::SchemaTable {
1065 name: #table_name.to_string(),
1066 columns,
1067 indexes,
1068 foreign_keys,
1069 create_sql: None,
1070 }
1071 }
1072 }
1073 })
1074}
1075
1076fn has_premix_field_flag(field: &Field, flag: &str) -> bool {
1077 for attr in &field.attrs {
1078 if attr.path().is_ident("premix")
1079 && let Ok(meta) = attr.parse_args::<syn::Ident>()
1080 && meta == flag
1081 {
1082 return true;
1083 }
1084 }
1085 false
1086}
1087
1088fn is_ignored(field: &Field) -> bool {
1089 has_premix_field_flag(field, "ignore")
1090}
1091
1092fn is_sensitive(field: &Field) -> bool {
1093 has_premix_field_flag(field, "sensitive")
1094}
1095
1096struct IndexSpec {
1097 name: String,
1098 columns: Vec<String>,
1099 unique: bool,
1100}
1101
1102struct ForeignKeySpec {
1103 column: String,
1104 ref_table: String,
1105 ref_column: String,
1106}
1107
1108fn collect_schema_specs(
1109 fields: &syn::punctuated::Punctuated<Field, Token![,]>,
1110 table_name: &str,
1111) -> syn::Result<(Vec<IndexSpec>, Vec<ForeignKeySpec>)> {
1112 let mut indexes = Vec::new();
1113 let mut foreign_keys = Vec::new();
1114
1115 for field in fields {
1116 if is_ignored(field) {
1117 continue;
1118 }
1119 let field_name = field
1120 .ident
1121 .as_ref()
1122 .ok_or_else(|| syn::Error::new_spanned(field, "Field must have an ident"))?
1123 .to_string();
1124
1125 for attr in &field.attrs {
1126 if !attr.path().is_ident("premix") {
1127 continue;
1128 }
1129
1130 attr.parse_nested_meta(|meta| {
1131 if meta.path.is_ident("index") || meta.path.is_ident("unique") {
1132 let unique = meta.path.is_ident("unique");
1133 let mut name = None;
1134 if meta.input.peek(syn::token::Paren) {
1135 meta.parse_nested_meta(|nested| {
1136 if nested.path.is_ident("name") {
1137 let lit: LitStr = nested.value()?.parse()?;
1138 name = Some(lit.value());
1139 Ok(())
1140 } else {
1141 Err(nested.error("unsupported index option"))
1142 }
1143 })?;
1144 }
1145 let index_name =
1146 name.unwrap_or_else(|| format!("idx_{}_{}", table_name, field_name));
1147 indexes.push(IndexSpec {
1148 name: index_name,
1149 columns: vec![field_name.clone()],
1150 unique,
1151 });
1152 } else if meta.path.is_ident("foreign_key") {
1153 let mut ref_table = None;
1154 let mut ref_column = None;
1155 meta.parse_nested_meta(|nested| {
1156 if nested.path.is_ident("table") {
1157 let lit: LitStr = nested.value()?.parse()?;
1158 ref_table = Some(lit.value());
1159 Ok(())
1160 } else if nested.path.is_ident("column") {
1161 let lit: LitStr = nested.value()?.parse()?;
1162 ref_column = Some(lit.value());
1163 Ok(())
1164 } else {
1165 Err(nested.error("unsupported foreign_key option"))
1166 }
1167 })?;
1168
1169 let ref_table = ref_table.ok_or_else(|| {
1170 syn::Error::new_spanned(attr, "foreign_key requires table = \"...\"")
1171 })?;
1172 let ref_column = ref_column.unwrap_or_else(|| "id".to_string());
1173 foreign_keys.push(ForeignKeySpec {
1174 column: field_name.clone(),
1175 ref_table,
1176 ref_column,
1177 });
1178 }
1179 Ok(())
1180 })?;
1181 }
1182 }
1183
1184 Ok((indexes, foreign_keys))
1185}
1186
1187fn is_option_type(ty: &syn::Type) -> bool {
1188 if let syn::Type::Path(path) = ty {
1189 if let Some(seg) = path.path.segments.last() {
1190 return seg.ident == "Option";
1191 }
1192 }
1193 false
1194}
1195
1196fn has_premix_flag(attrs: &[Attribute], flag: &str) -> bool {
1197 for attr in attrs {
1198 if attr.path().is_ident("premix") {
1199 let args = attr.parse_args_with(Punctuated::<Ident, Token![,]>::parse_terminated);
1200 if let Ok(args) = args {
1201 if args.iter().any(|ident| ident == flag) {
1202 return true;
1203 }
1204 }
1205 }
1206 }
1207 false
1208}
1209
1210fn type_name_for_field(ty: &syn::Type) -> Option<String> {
1211 if let syn::Type::Path(path) = ty {
1212 let segment = path.path.segments.last()?;
1213 let ident = segment.ident.to_string();
1214 if ident == "Option" {
1215 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
1216 for arg in args.args.iter() {
1217 if let syn::GenericArgument::Type(inner) = arg {
1218 return type_name_for_field(inner);
1219 }
1220 }
1221 }
1222 None
1223 } else if ident == "Vec" {
1224 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
1225 if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
1226 if let Some(inner_ident) = type_name_for_field(inner) {
1227 return Some(format!("Vec<{}>", inner_ident));
1228 }
1229 }
1230 }
1231 Some("Vec".to_string())
1232 } else {
1233 Some(ident)
1234 }
1235 } else {
1236 None
1237 }
1238}
1239
1240fn sql_type_for_field(name: &str, ty: &syn::Type) -> &'static str {
1241 let type_name = type_name_for_field(ty);
1242 match type_name.as_deref() {
1243 Some("i8" | "i16" | "i32" | "isize" | "u8" | "u16" | "u32" | "usize") => "INTEGER",
1244 Some("i64" | "u64") => "BIGINT",
1245 Some("f32" | "f64") => "REAL",
1246 Some("bool") => "BOOLEAN",
1247 Some("String" | "str") => "TEXT",
1248 Some("Uuid" | "DateTime" | "NaiveDateTime" | "NaiveDate") => "TEXT",
1249 Some("Vec<u8>") => "BLOB",
1250 _ => {
1251 if name == "id" || name.ends_with("_id") {
1252 "INTEGER"
1253 } else {
1254 "TEXT"
1255 }
1256 }
1257 }
1258}
1259
1260fn sql_type_expr_for_field(name: &str, ty: &syn::Type) -> proc_macro2::TokenStream {
1261 let type_name = type_name_for_field(ty);
1262 match type_name.as_deref() {
1263 Some("i8" | "i16" | "i32" | "isize" | "u8" | "u16" | "u32" | "usize") => {
1264 quote! { <DB as premix_orm::SqlDialect>::int_type() }
1265 }
1266 Some("i64" | "u64") => quote! { <DB as premix_orm::SqlDialect>::bigint_type() },
1267 Some("f32" | "f64") => quote! { <DB as premix_orm::SqlDialect>::float_type() },
1268 Some("bool") => quote! { <DB as premix_orm::SqlDialect>::bool_type() },
1269 Some("String" | "str") => quote! { <DB as premix_orm::SqlDialect>::text_type() },
1270 Some("Uuid" | "DateTime" | "NaiveDateTime" | "NaiveDate") => {
1271 quote! { <DB as premix_orm::SqlDialect>::text_type() }
1272 }
1273 Some("Vec<u8>") => quote! { <DB as premix_orm::SqlDialect>::blob_type() },
1274 _ => {
1275 if name == "id" || name.ends_with("_id") {
1276 quote! { <DB as premix_orm::SqlDialect>::int_type() }
1277 } else {
1278 quote! { <DB as premix_orm::SqlDialect>::text_type() }
1279 }
1280 }
1281 }
1282}
1283
1284#[cfg(test)]
1285mod tests {
1286 use syn::parse_quote;
1287
1288 use super::*;
1289
1290 #[test]
1291 fn generate_generic_impl_includes_table_and_columns() {
1292 let input: DeriveInput = parse_quote! {
1293 struct User {
1294 id: i32,
1295 name: String,
1296 version: i32,
1297 deleted_at: Option<String>,
1298 }
1299 };
1300 let tokens = generate_generic_impl(&input).unwrap().to_string();
1301 assert!(tokens.contains("CREATE TABLE IF NOT EXISTS"));
1302 assert!(tokens.contains("users"));
1303 assert!(tokens.contains("deleted_at"));
1304 assert!(tokens.contains("version"));
1305 }
1306
1307 #[test]
1308 fn generate_generic_impl_rejects_tuple_struct() {
1309 let input: DeriveInput = parse_quote! {
1310 struct User(i32, String);
1311 };
1312 let err = generate_generic_impl(&input).unwrap_err();
1313 assert!(err.to_string().contains("named fields"));
1314 }
1315
1316 #[test]
1317 fn generate_generic_impl_rejects_non_struct() {
1318 let input: DeriveInput = parse_quote! {
1319 enum User {
1320 A,
1321 B,
1322 }
1323 };
1324 let err = generate_generic_impl(&input).unwrap_err();
1325 assert!(err.to_string().contains("only supports structs"));
1326 }
1327
1328 #[test]
1329 fn generate_generic_impl_version_update_branch() {
1330 let input: DeriveInput = parse_quote! {
1331 struct User {
1332 id: i32,
1333 version: i32,
1334 name: String,
1335 }
1336 };
1337 let tokens = generate_generic_impl(&input).unwrap().to_string();
1338 assert!(tokens.contains("version = version + 1"));
1339 }
1340
1341 #[test]
1342 fn generate_generic_impl_no_version_branch() {
1343 let input: DeriveInput = parse_quote! {
1344 struct User {
1345 id: i32,
1346 name: String,
1347 }
1348 };
1349 let tokens = generate_generic_impl(&input).unwrap().to_string();
1350 assert!(!tokens.contains("version = version + 1"));
1351 }
1352
1353 #[test]
1354 fn generate_generic_impl_includes_default_hooks_and_validation() {
1355 let input: DeriveInput = parse_quote! {
1356 struct User {
1357 id: i32,
1358 name: String,
1359 }
1360 };
1361 let tokens = generate_generic_impl(&input).unwrap().to_string();
1362 assert!(tokens.contains("ModelHooks"));
1363 assert!(tokens.contains("ModelValidation"));
1364 }
1365
1366 #[test]
1367 fn generate_generic_impl_includes_schema_impl() {
1368 let input: DeriveInput = parse_quote! {
1369 struct User {
1370 id: i32,
1371 name: String,
1372 }
1373 };
1374 let tokens = generate_generic_impl(&input).unwrap().to_string();
1375 assert!(tokens.contains("ModelSchema"));
1376 assert!(tokens.contains("SchemaColumn"));
1377 }
1378
1379 #[test]
1380 fn generate_generic_impl_includes_index_and_foreign_key_metadata() {
1381 let input: DeriveInput = parse_quote! {
1382 struct User {
1383 id: i32,
1384 #[premix(index)]
1385 name: String,
1386 #[premix(unique(name = "users_email_uidx"))]
1387 email: String,
1388 #[premix(foreign_key(table = "accounts", column = "id"))]
1389 account_id: i32,
1390 }
1391 };
1392 let tokens = generate_generic_impl(&input).unwrap().to_string();
1393 assert!(tokens.contains("SchemaIndex"));
1394 assert!(tokens.contains("idx_users_name"));
1395 assert!(tokens.contains("users_email_uidx"));
1396 assert!(tokens.contains("SchemaForeignKey"));
1397 assert!(tokens.contains("accounts"));
1398 assert!(tokens.contains("account_id"));
1399 }
1400
1401 #[test]
1402 fn generate_generic_impl_includes_sensitive_fields() {
1403 let input: DeriveInput = parse_quote! {
1404 struct User {
1405 id: i32,
1406 #[premix(sensitive)]
1407 email: String,
1408 }
1409 };
1410 let tokens = generate_generic_impl(&input).unwrap().to_string();
1411 assert!(tokens.contains("sensitive_fields"));
1412 assert!(tokens.contains("\"email\""));
1413 }
1414
1415 #[test]
1416 fn generate_generic_impl_skips_custom_hooks_and_validation() {
1417 let input: DeriveInput = parse_quote! {
1418 #[premix(custom_hooks, custom_validation)]
1419 struct User {
1420 id: i32,
1421 name: String,
1422 }
1423 };
1424 let tokens = generate_generic_impl(&input).unwrap().to_string();
1425 assert!(!tokens.contains("impl premix_orm :: ModelHooks"));
1426 assert!(!tokens.contains("impl premix_orm :: ModelValidation"));
1427 }
1428
1429 #[test]
1430 fn is_ignored_detects_attribute() {
1431 let field: Field = parse_quote! {
1432 #[premix(ignore)]
1433 ignored: Option<String>
1434 };
1435 assert!(is_ignored(&field));
1436 }
1437
1438 #[test]
1439 fn is_ignored_false_for_other_attrs() {
1440 let field: Field = parse_quote! {
1441 #[serde(skip)]
1442 name: String
1443 };
1444 assert!(!is_ignored(&field));
1445 }
1446
1447 #[test]
1448 fn is_ignored_false_for_premix_other_arg() {
1449 let field: Field = parse_quote! {
1450 #[premix(skip)]
1451 name: String
1452 };
1453 assert!(!is_ignored(&field));
1454 }
1455
1456 #[test]
1457 fn is_sensitive_detects_attribute() {
1458 let field: Field = parse_quote! {
1459 #[premix(sensitive)]
1460 secret: String
1461 };
1462 assert!(is_sensitive(&field));
1463 }
1464
1465 #[test]
1466 fn is_sensitive_false_for_other_attrs() {
1467 let field: Field = parse_quote! {
1468 #[serde(skip)]
1469 secret: String
1470 };
1471 assert!(!is_sensitive(&field));
1472 }
1473
1474 #[test]
1475 fn is_ignored_false_when_premix_has_no_args() {
1476 let field: Field = parse_quote! {
1477 #[premix]
1478 name: String
1479 };
1480 assert!(!is_ignored(&field));
1481 }
1482
1483 #[test]
1484 fn derive_model_impl_emits_tokens() {
1485 let input: DeriveInput = parse_quote! {
1486 struct User {
1487 id: i32,
1488 name: String,
1489 }
1490 };
1491 let tokens = derive_model_impl(&input).unwrap().to_string();
1492 assert!(tokens.contains("impl"));
1493 }
1494
1495 #[test]
1496 fn derive_model_impl_propagates_error() {
1497 let input: DeriveInput = parse_quote! {
1498 enum User {
1499 A,
1500 }
1501 };
1502 let err = derive_model_impl(&input).unwrap_err();
1503 assert!(err.to_string().contains("only supports structs"));
1504 }
1505
1506 #[test]
1507 fn generate_generic_impl_includes_soft_delete_delete_impl() {
1508 let input: DeriveInput = parse_quote! {
1509 struct AuditLog {
1510 id: i32,
1511 deleted_at: Option<String>,
1512 }
1513 };
1514 let tokens = generate_generic_impl(&input).unwrap().to_string();
1515 assert!(tokens.contains("deleted_at ="));
1516 assert!(tokens.contains("has_soft_delete"));
1517 }
1518
1519 #[test]
1520 fn generate_generic_impl_ignores_marked_fields() {
1521 let input: DeriveInput = parse_quote! {
1522 struct User {
1523 id: i32,
1524 name: String,
1525 #[premix(ignore)]
1526 temp: Option<String>,
1527 }
1528 };
1529 let tokens = generate_generic_impl(&input).unwrap().to_string();
1530 assert!(tokens.contains("temp : None"));
1531 assert!(!tokens.contains("\"temp\""));
1532 }
1533
1534 #[test]
1535 fn generate_generic_impl_adds_relation_bounds() {
1536 let input: DeriveInput = parse_quote! {
1537 struct User {
1538 id: i32,
1539 #[has_many(Post)]
1540 posts: Vec<Post>,
1541 }
1542 };
1543 let tokens = generate_generic_impl(&input).unwrap().to_string();
1544 assert!(tokens.contains("Post : premix_orm :: Model < DB >"));
1545 }
1546
1547 #[test]
1548 fn generate_generic_impl_records_field_names() {
1549 let input: DeriveInput = parse_quote! {
1550 struct Account {
1551 id: i32,
1552 user_id: i32,
1553 is_active: bool,
1554 }
1555 };
1556 let tokens = generate_generic_impl(&input).unwrap().to_string();
1557 assert!(tokens.contains("\"user_id\""));
1558 assert!(tokens.contains("\"is_active\""));
1559 }
1560
1561 #[test]
1562 fn generate_generic_impl_creates_column_constants() {
1563 let input: DeriveInput = parse_quote! {
1564 struct User {
1565 id: i32,
1566 name: String,
1567 }
1568 };
1569 let tokens = generate_generic_impl(&input).unwrap().to_string();
1570 assert!(tokens.contains("pub mod columns_user"));
1571 assert!(tokens.contains("pub const ID : & str = \"id\""));
1572 assert!(tokens.contains("pub const NAME : & str = \"name\""));
1573 }
1574}