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;
9
10#[proc_macro_derive(Model, attributes(has_many, belongs_to, premix))]
11pub fn derive_model(input: TokenStream) -> TokenStream {
12 let input = parse_macro_input!(input as DeriveInput);
13 match derive_model_impl(&input) {
14 Ok(tokens) => TokenStream::from(tokens),
15 Err(err) => TokenStream::from(err.to_compile_error()),
16 }
17}
18
19fn derive_model_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
20 let impl_block = generate_generic_impl(input)?;
21 let rel_block = relations::impl_relations(input)?;
22 Ok(quote! {
23 #impl_block
24 #rel_block
25 })
26}
27
28#[cfg(test)]
29mod tests {
30 use syn::parse_quote;
31
32 use super::*;
33
34 #[test]
35 fn generate_generic_impl_includes_table_and_columns() {
36 let input: DeriveInput = parse_quote! {
37 struct User {
38 id: i32,
39 name: String,
40 version: i32,
41 deleted_at: Option<String>,
42 }
43 };
44 let tokens = generate_generic_impl(&input).unwrap().to_string();
45 assert!(tokens.contains("CREATE TABLE IF NOT EXISTS"));
46 assert!(tokens.contains("users"));
47 assert!(tokens.contains("deleted_at"));
48 assert!(tokens.contains("version"));
49 }
50
51 #[test]
52 fn generate_generic_impl_rejects_tuple_struct() {
53 let input: DeriveInput = parse_quote! {
54 struct User(i32, String);
55 };
56 let err = generate_generic_impl(&input).unwrap_err();
57 assert!(err.to_string().contains("named fields"));
58 }
59
60 #[test]
61 fn generate_generic_impl_rejects_non_struct() {
62 let input: DeriveInput = parse_quote! {
63 enum User {
64 A,
65 B,
66 }
67 };
68 let err = generate_generic_impl(&input).unwrap_err();
69 assert!(err.to_string().contains("only supports structs"));
70 }
71
72 #[test]
73 fn generate_generic_impl_version_update_branch() {
74 let input: DeriveInput = parse_quote! {
75 struct User {
76 id: i32,
77 version: i32,
78 name: String,
79 }
80 };
81 let tokens = generate_generic_impl(&input).unwrap().to_string();
82 assert!(tokens.contains("version = version + 1"));
83 }
84
85 #[test]
86 fn generate_generic_impl_no_version_branch() {
87 let input: DeriveInput = parse_quote! {
88 struct User {
89 id: i32,
90 name: String,
91 }
92 };
93 let tokens = generate_generic_impl(&input).unwrap().to_string();
94 assert!(!tokens.contains("version = version + 1"));
95 }
96
97 #[test]
98 fn generate_generic_impl_includes_default_hooks_and_validation() {
99 let input: DeriveInput = parse_quote! {
100 struct User {
101 id: i32,
102 name: String,
103 }
104 };
105 let tokens = generate_generic_impl(&input).unwrap().to_string();
106 assert!(tokens.contains("ModelHooks"));
107 assert!(tokens.contains("ModelValidation"));
108 }
109
110 #[test]
111 fn generate_generic_impl_includes_schema_impl() {
112 let input: DeriveInput = parse_quote! {
113 struct User {
114 id: i32,
115 name: String,
116 }
117 };
118 let tokens = generate_generic_impl(&input).unwrap().to_string();
119 assert!(tokens.contains("ModelSchema"));
120 assert!(tokens.contains("SchemaColumn"));
121 }
122
123 #[test]
124 fn generate_generic_impl_includes_index_and_foreign_key_metadata() {
125 let input: DeriveInput = parse_quote! {
126 struct User {
127 id: i32,
128 #[premix(index)]
129 name: String,
130 #[premix(unique(name = "users_email_uidx"))]
131 email: String,
132 #[premix(foreign_key(table = "accounts", column = "id"))]
133 account_id: i32,
134 }
135 };
136 let tokens = generate_generic_impl(&input).unwrap().to_string();
137 assert!(tokens.contains("SchemaIndex"));
138 assert!(tokens.contains("idx_users_name"));
139 assert!(tokens.contains("users_email_uidx"));
140 assert!(tokens.contains("SchemaForeignKey"));
141 assert!(tokens.contains("accounts"));
142 assert!(tokens.contains("account_id"));
143 }
144
145 #[test]
146 fn generate_generic_impl_includes_sensitive_fields() {
147 let input: DeriveInput = parse_quote! {
148 struct User {
149 id: i32,
150 #[premix(sensitive)]
151 email: String,
152 }
153 };
154 let tokens = generate_generic_impl(&input).unwrap().to_string();
155 assert!(tokens.contains("sensitive_fields"));
156 assert!(tokens.contains("\"email\""));
157 }
158
159 #[test]
160 fn generate_generic_impl_skips_custom_hooks_and_validation() {
161 let input: DeriveInput = parse_quote! {
162 #[premix(custom_hooks, custom_validation)]
163 struct User {
164 id: i32,
165 name: String,
166 }
167 };
168 let tokens = generate_generic_impl(&input).unwrap().to_string();
169 assert!(!tokens.contains("impl premix_orm :: ModelHooks"));
170 assert!(!tokens.contains("impl premix_orm :: ModelValidation"));
171 }
172
173 #[test]
174 fn is_ignored_detects_attribute() {
175 let field: Field = parse_quote! {
176 #[premix(ignore)]
177 ignored: Option<String>
178 };
179 assert!(is_ignored(&field));
180 }
181
182 #[test]
183 fn is_ignored_false_for_other_attrs() {
184 let field: Field = parse_quote! {
185 #[serde(skip)]
186 name: String
187 };
188 assert!(!is_ignored(&field));
189 }
190
191 #[test]
192 fn is_ignored_false_for_premix_other_arg() {
193 let field: Field = parse_quote! {
194 #[premix(skip)]
195 name: String
196 };
197 assert!(!is_ignored(&field));
198 }
199
200 #[test]
201 fn is_sensitive_detects_attribute() {
202 let field: Field = parse_quote! {
203 #[premix(sensitive)]
204 secret: String
205 };
206 assert!(is_sensitive(&field));
207 }
208
209 #[test]
210 fn is_sensitive_false_for_other_attrs() {
211 let field: Field = parse_quote! {
212 #[serde(skip)]
213 secret: String
214 };
215 assert!(!is_sensitive(&field));
216 }
217
218 #[test]
219 fn is_ignored_false_when_premix_has_no_args() {
220 let field: Field = parse_quote! {
221 #[premix]
222 name: String
223 };
224 assert!(!is_ignored(&field));
225 }
226
227 #[test]
228 fn derive_model_impl_emits_tokens() {
229 let input: DeriveInput = parse_quote! {
230 struct User {
231 id: i32,
232 name: String,
233 }
234 };
235 let tokens = derive_model_impl(&input).unwrap().to_string();
236 assert!(tokens.contains("impl"));
237 }
238
239 #[test]
240 fn derive_model_impl_propagates_error() {
241 let input: DeriveInput = parse_quote! {
242 enum User {
243 A,
244 }
245 };
246 let err = derive_model_impl(&input).unwrap_err();
247 assert!(err.to_string().contains("only supports structs"));
248 }
249
250 #[test]
251 fn generate_generic_impl_includes_soft_delete_delete_impl() {
252 let input: DeriveInput = parse_quote! {
253 struct AuditLog {
254 id: i32,
255 deleted_at: Option<String>,
256 }
257 };
258 let tokens = generate_generic_impl(&input).unwrap().to_string();
259 assert!(tokens.contains("deleted_at ="));
260 assert!(tokens.contains("has_soft_delete"));
261 }
262
263 #[test]
264 fn generate_generic_impl_ignores_marked_fields() {
265 let input: DeriveInput = parse_quote! {
266 struct User {
267 id: i32,
268 name: String,
269 #[premix(ignore)]
270 temp: Option<String>,
271 }
272 };
273 let tokens = generate_generic_impl(&input).unwrap().to_string();
274 assert!(tokens.contains("temp : None"));
275 assert!(!tokens.contains("\"temp\""));
276 }
277
278 #[test]
279 fn generate_generic_impl_adds_relation_bounds() {
280 let input: DeriveInput = parse_quote! {
281 struct User {
282 id: i32,
283 #[has_many(Post)]
284 posts: Vec<Post>,
285 }
286 };
287 let tokens = generate_generic_impl(&input).unwrap().to_string();
288 assert!(tokens.contains("Post : premix_orm :: Model < DB >"));
289 }
290
291 #[test]
292 fn generate_generic_impl_records_field_names() {
293 let input: DeriveInput = parse_quote! {
294 struct Account {
295 id: i32,
296 user_id: i32,
297 is_active: bool,
298 }
299 };
300 let tokens = generate_generic_impl(&input).unwrap().to_string();
301 assert!(tokens.contains("\"user_id\""));
302 assert!(tokens.contains("\"is_active\""));
303 }
304}
305
306fn generate_generic_impl(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
307 let struct_name = &input.ident;
308 let table_name = struct_name.to_string().to_lowercase() + "s";
309 let custom_hooks = has_premix_flag(&input.attrs, "custom_hooks");
310 let custom_validation = has_premix_flag(&input.attrs, "custom_validation");
311
312 let all_fields = if let Data::Struct(data) = &input.data {
313 if let Fields::Named(fields) = &data.fields {
314 &fields.named
315 } else {
316 return Err(syn::Error::new_spanned(
317 &data.fields,
318 "Premix Model only supports structs with named fields",
319 ));
320 }
321 } else {
322 return Err(syn::Error::new_spanned(
323 input,
324 "Premix Model only supports structs",
325 ));
326 };
327
328 let mut db_fields = Vec::new();
329 let mut ignored_field_idents = Vec::new();
330
331 for field in all_fields {
332 if is_ignored(field) {
333 ignored_field_idents.push(field.ident.as_ref().unwrap());
334 } else {
335 db_fields.push(field);
336 }
337 }
338
339 let field_idents: Vec<_> = db_fields
340 .iter()
341 .map(|f| f.ident.as_ref().unwrap())
342 .collect();
343 let field_types: Vec<_> = db_fields.iter().map(|f| &f.ty).collect();
344 let _field_indices: Vec<_> = (0..db_fields.len()).collect();
345 let field_names: Vec<_> = field_idents.iter().map(|id| id.to_string()).collect();
346 let field_names_no_id: Vec<_> = field_names
347 .iter()
348 .filter(|name| *name != "id")
349 .cloned()
350 .collect();
351 let field_names_no_id_len = field_names_no_id.len();
352 let all_columns_joined = field_names.join(", ");
353 let no_id_columns_joined = field_names_no_id.join(", ");
354 let field_idents_len = field_idents.len();
355 let field_nullables: Vec<_> = db_fields.iter().map(|f| is_option_type(&f.ty)).collect();
356 let field_primary_keys: Vec<_> = field_names.iter().map(|n| n == "id").collect();
357 let field_sql_types: Vec<_> = db_fields
358 .iter()
359 .map(|field| {
360 let name = field.ident.as_ref().unwrap().to_string();
361 sql_type_for_field(&name, &field.ty).to_string()
362 })
363 .collect();
364 let field_sql_type_exprs: Vec<_> = db_fields
365 .iter()
366 .map(|field| {
367 let name = field.ident.as_ref().unwrap().to_string();
368 sql_type_expr_for_field(&name, &field.ty)
369 })
370 .collect();
371 let sensitive_field_literals: Vec<LitStr> = db_fields
372 .iter()
373 .filter(|f| is_sensitive(f))
374 .map(|f| {
375 LitStr::new(
376 &f.ident.as_ref().unwrap().to_string(),
377 f.ident.as_ref().unwrap().span(),
378 )
379 })
380 .collect();
381
382 let eager_load_body = relations::generate_eager_load_body(input)?;
383 let (index_specs, foreign_key_specs) = collect_schema_specs(all_fields, &table_name)?;
384 let index_tokens: Vec<_> = index_specs
385 .iter()
386 .map(|spec| {
387 let name = &spec.name;
388 let columns = &spec.columns;
389 let unique = spec.unique;
390 quote! {
391 premix_orm::schema::SchemaIndex {
392 name: #name.to_string(),
393 columns: vec![#(#columns.to_string()),*],
394 unique: #unique,
395 }
396 }
397 })
398 .collect();
399 let foreign_key_tokens: Vec<_> = foreign_key_specs
400 .iter()
401 .map(|spec| {
402 let column = &spec.column;
403 let ref_table = &spec.ref_table;
404 let ref_column = &spec.ref_column;
405 quote! {
406 premix_orm::schema::SchemaForeignKey {
407 column: #column.to_string(),
408 ref_table: #ref_table.to_string(),
409 ref_column: #ref_column.to_string(),
410 }
411 }
412 })
413 .collect();
414 let has_version = field_names.contains(&"version".to_string());
415 let has_soft_delete = field_names.contains(&"deleted_at".to_string());
416
417 let update_impl = if has_version {
418 quote! {
419 fn update<'a, E>(
420 &'a mut self,
421 executor: E,
422 ) -> impl ::std::future::Future<
423 Output = Result<premix_orm::UpdateResult, premix_orm::sqlx::Error>,
424 > + Send
425 where
426 E: premix_orm::IntoExecutor<'a, DB = DB>
427 {
428 async move {
429 let mut executor = executor.into_executor();
430 let table_name = Self::table_name();
431 let mut set_clause = String::new();
432 let mut i = 1usize;
433 #(
434 if i > 1 {
435 set_clause.push_str(", ");
436 }
437 set_clause.push_str(#field_names);
438 set_clause.push_str(" = ");
439 set_clause.push_str(&<DB as premix_orm::SqlDialect>::placeholder(i));
440 i += 1;
441 )*
442 let id_p = <DB as premix_orm::SqlDialect>::placeholder(1 + #field_idents_len);
443 let ver_p = <DB as premix_orm::SqlDialect>::placeholder(2 + #field_idents_len);
444 let sql = format!(
445 "UPDATE {} SET {}, version = version + 1 WHERE id = {} AND version = {}",
446 table_name, set_clause, id_p, ver_p
447 );
448
449 premix_orm::tracing::debug!(
450 operation = "update",
451 table = table_name,
452 sql = %sql,
453 "premix query"
454 );
455
456 let mut query = premix_orm::sqlx::query::<DB>(&sql)
457 #( .bind(&self.#field_idents) )*
458 .bind(&self.id)
459 .bind(&self.version);
460
461 let result = executor.execute(query).await?;
462
463 if <DB as premix_orm::SqlDialect>::rows_affected(&result) == 0 {
464 let exists_p = <DB as premix_orm::SqlDialect>::placeholder(1);
465 let exists_sql = format!("SELECT id FROM {} WHERE id = {}", table_name, exists_p);
466 let exists_query = premix_orm::sqlx::query_as::<DB, (i32,)>(&exists_sql).bind(&self.id);
467 let exists = executor.fetch_optional(exists_query).await?;
468
469 if exists.is_none() {
470 Ok(premix_orm::UpdateResult::NotFound)
471 } else {
472 Ok(premix_orm::UpdateResult::VersionConflict)
473 }
474 } else {
475 self.version += 1;
476 Ok(premix_orm::UpdateResult::Success)
477 }
478 }
479 }
480 }
481 } else {
482 quote! {
483 fn update<'a, E>(
484 &'a mut self,
485 executor: E,
486 ) -> impl ::std::future::Future<
487 Output = Result<premix_orm::UpdateResult, premix_orm::sqlx::Error>,
488 > + Send
489 where
490 E: premix_orm::IntoExecutor<'a, DB = DB>
491 {
492 async move {
493 let mut executor = executor.into_executor();
494 let table_name = Self::table_name();
495 let mut set_clause = String::new();
496 let mut i = 1usize;
497 #(
498 if i > 1 {
499 set_clause.push_str(", ");
500 }
501 set_clause.push_str(#field_names);
502 set_clause.push_str(" = ");
503 set_clause.push_str(&<DB as premix_orm::SqlDialect>::placeholder(i));
504 i += 1;
505 )*
506 let id_p = <DB as premix_orm::SqlDialect>::placeholder(1 + #field_idents_len);
507 let sql = format!("UPDATE {} SET {} WHERE id = {}", table_name, set_clause, id_p);
508
509 premix_orm::tracing::debug!(
510 operation = "update",
511 table = table_name,
512 sql = %sql,
513 "premix query"
514 );
515
516 let mut query = premix_orm::sqlx::query::<DB>(&sql)
517 #( .bind(&self.#field_idents) )*
518 .bind(&self.id);
519
520 let result = executor.execute(query).await?;
521
522 if <DB as premix_orm::SqlDialect>::rows_affected(&result) == 0 {
523 Ok(premix_orm::UpdateResult::NotFound)
524 } else {
525 Ok(premix_orm::UpdateResult::Success)
526 }
527 }
528 }
529 }
530 };
531
532 let delete_impl = if has_soft_delete {
533 quote! {
534 fn delete<'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 let id_p = <DB as premix_orm::SqlDialect>::placeholder(1);
546 let sql = format!("UPDATE {} SET deleted_at = {} WHERE id = {}", table_name, <DB as premix_orm::SqlDialect>::current_timestamp_fn(), id_p);
547
548 premix_orm::tracing::debug!(
549 operation = "delete",
550 table = table_name,
551 sql = %sql,
552 "premix query"
553 );
554
555 let query = premix_orm::sqlx::query::<DB>(&sql).bind(&self.id);
556 executor.execute(query).await?;
557
558 self.deleted_at = Some("DELETED".to_string());
559 Ok(())
560 }
561 }
562 fn has_soft_delete() -> bool { true }
563 }
564 } else {
565 quote! {
566 fn delete<'a, E>(
567 &'a mut self,
568 executor: E,
569 ) -> impl ::std::future::Future<Output = Result<(), premix_orm::sqlx::Error>>
570 + Send
571 where
572 E: premix_orm::IntoExecutor<'a, DB = DB>
573 {
574 async move {
575 let mut executor = executor.into_executor();
576 let table_name = Self::table_name();
577 let id_p = <DB as premix_orm::SqlDialect>::placeholder(1);
578 let sql = format!("DELETE FROM {} WHERE id = {}", table_name, id_p);
579
580 premix_orm::tracing::debug!(
581 operation = "delete",
582 table = table_name,
583 sql = %sql,
584 "premix query"
585 );
586
587 let query = premix_orm::sqlx::query::<DB>(&sql).bind(&self.id);
588 executor.execute(query).await?;
589
590 Ok(())
591 }
592 }
593 fn has_soft_delete() -> bool { false }
594 }
595 };
596
597 let mut related_model_bounds = Vec::new();
598 for field in all_fields {
599 for attr in &field.attrs {
600 if attr.path().is_ident("has_many")
601 && let Ok(related_ident) = attr.parse_args::<syn::Ident>()
602 {
603 related_model_bounds.push(quote! { #related_ident: premix_orm::Model<DB> });
604 } else if attr.path().is_ident("belongs_to")
605 && let Ok(related_ident) = attr.parse_args::<syn::Ident>()
606 {
607 related_model_bounds.push(quote! { #related_ident: premix_orm::Model<DB> + Clone });
608 }
609 }
610 }
611
612 let hooks_impl = if custom_hooks {
613 quote! {}
614 } else {
615 quote! {
616 impl premix_orm::ModelHooks for #struct_name {}
617 }
618 };
619
620 let validation_impl = if custom_validation {
621 quote! {}
622 } else {
623 quote! {
624 impl premix_orm::ModelValidation for #struct_name {}
625 }
626 };
627
628 Ok(quote! {
630 impl<'r, R> premix_orm::sqlx::FromRow<'r, R> for #struct_name
631 where
632 R: premix_orm::sqlx::Row,
633 R::Database: premix_orm::sqlx::Database,
634 #(
635 #field_types: premix_orm::sqlx::Type<R::Database> + premix_orm::sqlx::Decode<'r, R::Database>,
636 )*
637 for<'c> &'c str: premix_orm::sqlx::ColumnIndex<R>,
638 {
639 fn from_row(row: &'r R) -> Result<Self, premix_orm::sqlx::Error> {
640 use premix_orm::sqlx::Row;
641 Ok(Self {
642 #(
643 #field_idents: row.try_get(#field_names)?,
644 )*
645 #(
646 #ignored_field_idents: None,
647 )*
648 })
649 }
650 }
651
652
653 impl<DB> premix_orm::Model<DB> for #struct_name
654 where
655 DB: premix_orm::SqlDialect,
656 for<'c> &'c str: premix_orm::sqlx::ColumnIndex<DB::Row>,
657 usize: premix_orm::sqlx::ColumnIndex<DB::Row>,
658 for<'q> <DB as premix_orm::sqlx::Database>::Arguments<'q>: premix_orm::sqlx::IntoArguments<'q, DB>,
659 for<'c> &'c mut <DB as premix_orm::sqlx::Database>::Connection: premix_orm::sqlx::Executor<'c, Database = DB>,
660 i32: premix_orm::sqlx::Type<DB> + for<'q> premix_orm::sqlx::Encode<'q, DB> + for<'r> premix_orm::sqlx::Decode<'r, DB>,
661 i64: premix_orm::sqlx::Type<DB> + for<'q> premix_orm::sqlx::Encode<'q, DB> + for<'r> premix_orm::sqlx::Decode<'r, DB>,
662 String: premix_orm::sqlx::Type<DB> + for<'q> premix_orm::sqlx::Encode<'q, DB> + for<'r> premix_orm::sqlx::Decode<'r, DB>,
663 bool: premix_orm::sqlx::Type<DB> + for<'q> premix_orm::sqlx::Encode<'q, DB> + for<'r> premix_orm::sqlx::Decode<'r, DB>,
664 Option<String>: premix_orm::sqlx::Type<DB> + for<'q> premix_orm::sqlx::Encode<'q, DB> + for<'r> premix_orm::sqlx::Decode<'r, DB>,
665 #( #related_model_bounds, )*
666 {
667 fn table_name() -> &'static str {
668 #table_name
669 }
670
671 fn create_table_sql() -> String {
672 let mut cols = vec!["id ".to_string() + <DB as premix_orm::SqlDialect>::auto_increment_pk()];
673 #(
674 if #field_names != "id" {
675 let sql_type = #field_sql_type_exprs;
676 cols.push(format!("{} {}", #field_names, sql_type));
677 }
678 )*
679 format!("CREATE TABLE IF NOT EXISTS {} ({})", #table_name, cols.join(", "))
680 }
681
682 fn list_columns() -> Vec<String> {
683 vec![ #( #field_names.to_string() ),* ]
684 }
685
686 fn sensitive_fields() -> &'static [&'static str] {
687 &[ #( #sensitive_field_literals ),* ]
688 }
689
690 fn save<'a, E>(
691 &'a mut self,
692 executor: E,
693 ) -> impl ::std::future::Future<Output = Result<(), premix_orm::sqlx::Error>>
694 + Send
695 where
696 E: premix_orm::IntoExecutor<'a, DB = DB>
697 {
698 async move {
699 let mut executor = executor.into_executor();
700 use premix_orm::ModelHooks;
701 self.before_save().await?;
702
703 const ALL_COLUMNS: [&str; #field_idents_len] = [#( #field_names ),*];
705 const NO_ID_COLUMNS: [&str; #field_names_no_id_len] = [#( #field_names_no_id ),*];
706 let columns: &[&str] = if self.id == 0 { &NO_ID_COLUMNS } else { &ALL_COLUMNS };
707 let column_list: &str = if self.id == 0 { #no_id_columns_joined } else { #all_columns_joined };
708 let placeholders = premix_orm::build_placeholders::<DB>(1, columns.len());
709
710 let supports_returning = <DB as premix_orm::SqlDialect>::supports_returning();
711 if supports_returning {
712 let sql = format!(
713 "INSERT INTO {} ({}) VALUES ({}) RETURNING id",
714 #table_name,
715 column_list,
716 placeholders
717 );
718
719 premix_orm::tracing::debug!(
720 operation = "insert",
721 table = #table_name,
722 sql = %sql,
723 "premix query"
724 );
725
726 let mut query = premix_orm::sqlx::query_as::<DB, (i32,)>(&sql);
727 #(
728 if #field_names != "id" {
729 query = query.bind(&self.#field_idents);
730 } else if self.id != 0 {
731 query = query.bind(&self.id);
732 }
733 )*
734
735 if let Some((id,)) = executor.fetch_optional(query).await? {
736 self.id = id;
737 }
738 } else {
739 let sql = format!(
740 "INSERT INTO {} ({}) VALUES ({})",
741 #table_name,
742 column_list,
743 placeholders
744 );
745
746 premix_orm::tracing::debug!(
747 operation = "insert",
748 table = #table_name,
749 sql = %sql,
750 "premix query"
751 );
752
753 let mut query = premix_orm::sqlx::query::<DB>(&sql);
754 #(
755 if #field_names != "id" {
756 query = query.bind(&self.#field_idents);
757 } else if self.id != 0 {
758 query = query.bind(&self.id);
759 }
760 )*
761
762 let result = executor.execute(query).await?;
763 let last_id = <DB as premix_orm::SqlDialect>::last_insert_id(&result);
764 if last_id > 0 {
765 self.id = last_id as i32;
766 }
767 }
768
769 self.after_save().await?;
770 Ok(())
771 }
772 }
773
774 #update_impl
775 #delete_impl
776
777 fn find_by_id<'a, E>(
778 executor: E,
779 id: i32,
780 ) -> impl ::std::future::Future<Output = Result<Option<Self>, premix_orm::sqlx::Error>>
781 + Send
782 where
783 E: premix_orm::IntoExecutor<'a, DB = DB>
784 {
785 async move {
786 let mut executor = executor.into_executor();
787 let p = <DB as premix_orm::SqlDialect>::placeholder(1);
788 let mut where_clause = format!("WHERE id = {}", p);
789 if Self::has_soft_delete() {
790 where_clause.push_str(" AND deleted_at IS NULL");
791 }
792 let sql = format!("SELECT * FROM {} {} LIMIT 1", #table_name, where_clause);
793 premix_orm::tracing::debug!(
794 operation = "select",
795 table = #table_name,
796 sql = %sql,
797 "premix query"
798 );
799 let query = premix_orm::sqlx::query_as::<DB, Self>(&sql).bind(id);
800
801 executor.fetch_optional(query).await
802 }
803 }
804
805 fn eager_load<'a>(
806 models: &mut [Self],
807 relation: &str,
808 executor: premix_orm::Executor<'a, DB>,
809 ) -> impl ::std::future::Future<Output = Result<(), premix_orm::sqlx::Error>> + Send
810 {
811 async move {
812 let mut executor = executor;
813 #eager_load_body
814 }
815 }
816 }
817
818 #hooks_impl
819 #validation_impl
820
821 impl premix_orm::ModelSchema for #struct_name {
822 fn schema() -> premix_orm::schema::SchemaTable {
823 let columns = vec![
824 #(
825 premix_orm::schema::SchemaColumn {
826 name: #field_names.to_string(),
827 sql_type: #field_sql_types.to_string(),
828 nullable: #field_nullables,
829 primary_key: #field_primary_keys,
830 }
831 ),*
832 ];
833 let indexes = vec![
834 #(#index_tokens),*
835 ];
836 let foreign_keys = vec![
837 #(#foreign_key_tokens),*
838 ];
839 premix_orm::schema::SchemaTable {
840 name: #table_name.to_string(),
841 columns,
842 indexes,
843 foreign_keys,
844 create_sql: None,
845 }
846 }
847 }
848 })
849}
850
851fn has_premix_field_flag(field: &Field, flag: &str) -> bool {
852 for attr in &field.attrs {
853 if attr.path().is_ident("premix")
854 && let Ok(meta) = attr.parse_args::<syn::Ident>()
855 && meta == flag
856 {
857 return true;
858 }
859 }
860 false
861}
862
863fn is_ignored(field: &Field) -> bool {
864 has_premix_field_flag(field, "ignore")
865}
866
867fn is_sensitive(field: &Field) -> bool {
868 has_premix_field_flag(field, "sensitive")
869}
870
871struct IndexSpec {
872 name: String,
873 columns: Vec<String>,
874 unique: bool,
875}
876
877struct ForeignKeySpec {
878 column: String,
879 ref_table: String,
880 ref_column: String,
881}
882
883fn collect_schema_specs(
884 fields: &syn::punctuated::Punctuated<Field, Token![,]>,
885 table_name: &str,
886) -> syn::Result<(Vec<IndexSpec>, Vec<ForeignKeySpec>)> {
887 let mut indexes = Vec::new();
888 let mut foreign_keys = Vec::new();
889
890 for field in fields {
891 if is_ignored(field) {
892 continue;
893 }
894 let field_name = field
895 .ident
896 .as_ref()
897 .ok_or_else(|| syn::Error::new_spanned(field, "Field must have an ident"))?
898 .to_string();
899
900 for attr in &field.attrs {
901 if !attr.path().is_ident("premix") {
902 continue;
903 }
904
905 attr.parse_nested_meta(|meta| {
906 if meta.path.is_ident("index") || meta.path.is_ident("unique") {
907 let unique = meta.path.is_ident("unique");
908 let mut name = None;
909 if meta.input.peek(syn::token::Paren) {
910 meta.parse_nested_meta(|nested| {
911 if nested.path.is_ident("name") {
912 let lit: LitStr = nested.value()?.parse()?;
913 name = Some(lit.value());
914 Ok(())
915 } else {
916 Err(nested.error("unsupported index option"))
917 }
918 })?;
919 }
920 let index_name =
921 name.unwrap_or_else(|| format!("idx_{}_{}", table_name, field_name));
922 indexes.push(IndexSpec {
923 name: index_name,
924 columns: vec![field_name.clone()],
925 unique,
926 });
927 } else if meta.path.is_ident("foreign_key") {
928 let mut ref_table = None;
929 let mut ref_column = None;
930 meta.parse_nested_meta(|nested| {
931 if nested.path.is_ident("table") {
932 let lit: LitStr = nested.value()?.parse()?;
933 ref_table = Some(lit.value());
934 Ok(())
935 } else if nested.path.is_ident("column") {
936 let lit: LitStr = nested.value()?.parse()?;
937 ref_column = Some(lit.value());
938 Ok(())
939 } else {
940 Err(nested.error("unsupported foreign_key option"))
941 }
942 })?;
943
944 let ref_table = ref_table.ok_or_else(|| {
945 syn::Error::new_spanned(attr, "foreign_key requires table = \"...\"")
946 })?;
947 let ref_column = ref_column.unwrap_or_else(|| "id".to_string());
948 foreign_keys.push(ForeignKeySpec {
949 column: field_name.clone(),
950 ref_table,
951 ref_column,
952 });
953 }
954 Ok(())
955 })?;
956 }
957 }
958
959 Ok((indexes, foreign_keys))
960}
961
962fn is_option_type(ty: &syn::Type) -> bool {
963 if let syn::Type::Path(path) = ty {
964 if let Some(seg) = path.path.segments.last() {
965 return seg.ident == "Option";
966 }
967 }
968 false
969}
970
971fn has_premix_flag(attrs: &[Attribute], flag: &str) -> bool {
972 for attr in attrs {
973 if attr.path().is_ident("premix") {
974 let args = attr.parse_args_with(Punctuated::<Ident, Token![,]>::parse_terminated);
975 if let Ok(args) = args {
976 if args.iter().any(|ident| ident == flag) {
977 return true;
978 }
979 }
980 }
981 }
982 false
983}
984
985fn type_name_for_field(ty: &syn::Type) -> Option<String> {
986 if let syn::Type::Path(path) = ty {
987 let segment = path.path.segments.last()?;
988 let ident = segment.ident.to_string();
989 if ident == "Option" {
990 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
991 for arg in args.args.iter() {
992 if let syn::GenericArgument::Type(inner) = arg {
993 return type_name_for_field(inner);
994 }
995 }
996 }
997 None
998 } else if ident == "Vec" {
999 if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
1000 if let Some(syn::GenericArgument::Type(inner)) = args.args.first() {
1001 if let Some(inner_ident) = type_name_for_field(inner) {
1002 return Some(format!("Vec<{}>", inner_ident));
1003 }
1004 }
1005 }
1006 Some("Vec".to_string())
1007 } else {
1008 Some(ident)
1009 }
1010 } else {
1011 None
1012 }
1013}
1014
1015fn sql_type_for_field(name: &str, ty: &syn::Type) -> &'static str {
1016 let type_name = type_name_for_field(ty);
1017 match type_name.as_deref() {
1018 Some("i8" | "i16" | "i32" | "isize" | "u8" | "u16" | "u32" | "usize") => "INTEGER",
1019 Some("i64" | "u64") => "BIGINT",
1020 Some("f32" | "f64") => "REAL",
1021 Some("bool") => "BOOLEAN",
1022 Some("String" | "str") => "TEXT",
1023 Some("Uuid" | "DateTime" | "NaiveDateTime" | "NaiveDate") => "TEXT",
1024 Some("Vec<u8>") => "BLOB",
1025 _ => {
1026 if name == "id" || name.ends_with("_id") {
1027 "INTEGER"
1028 } else {
1029 "TEXT"
1030 }
1031 }
1032 }
1033}
1034
1035fn sql_type_expr_for_field(name: &str, ty: &syn::Type) -> proc_macro2::TokenStream {
1036 let type_name = type_name_for_field(ty);
1037 match type_name.as_deref() {
1038 Some("i8" | "i16" | "i32" | "isize" | "u8" | "u16" | "u32" | "usize") => {
1039 quote! { <DB as premix_orm::SqlDialect>::int_type() }
1040 }
1041 Some("i64" | "u64") => quote! { <DB as premix_orm::SqlDialect>::bigint_type() },
1042 Some("f32" | "f64") => quote! { <DB as premix_orm::SqlDialect>::float_type() },
1043 Some("bool") => quote! { <DB as premix_orm::SqlDialect>::bool_type() },
1044 Some("String" | "str") => quote! { <DB as premix_orm::SqlDialect>::text_type() },
1045 Some("Uuid" | "DateTime" | "NaiveDateTime" | "NaiveDate") => {
1046 quote! { <DB as premix_orm::SqlDialect>::text_type() }
1047 }
1048 Some("Vec<u8>") => quote! { <DB as premix_orm::SqlDialect>::blob_type() },
1049 _ => {
1050 if name == "id" || name.ends_with("_id") {
1051 quote! { <DB as premix_orm::SqlDialect>::int_type() }
1052 } else {
1053 quote! { <DB as premix_orm::SqlDialect>::text_type() }
1054 }
1055 }
1056 }
1057}