1use crate::error::Result;
4use std::collections::{HashMap, HashSet};
5use std::fs;
6use std::path::Path;
7use tracing::debug;
8
9use crate::config::CodegenConfig;
10use crate::parser::{ColumnMetadata, TableMetadata};
11
12use super::naming::{
13 escape_field_name, generate_count_by_method_name, generate_delete_by_method_name,
14 generate_find_by_list_method_name, generate_find_by_method_name,
15 generate_update_by_method_name, generate_update_partial_by_method_name, pluralize,
16 to_struct_name,
17};
18use super::type_resolver::TypeResolver;
19
20const PRIORITY_PRIMARY_KEY: u8 = 1;
22const PRIORITY_UNIQUE_INDEX: u8 = 2;
23const PRIORITY_NON_UNIQUE_INDEX: u8 = 3;
24const PRIORITY_FOREIGN_KEY: u8 = 4;
25
26#[derive(Debug, Clone)]
28struct MethodSignature {
29 columns: Vec<String>,
30 method_name: String,
31 priority: u8,
32 is_unique: bool,
33 source: String,
34}
35
36impl MethodSignature {
37 fn new(columns: Vec<String>, priority: u8, is_unique: bool, source: &str) -> Self {
38 let method_name = generate_find_by_method_name(&columns);
39 Self {
40 columns,
41 method_name,
42 priority,
43 is_unique,
44 source: source.to_string(),
45 }
46 }
47}
48
49pub fn generate_daos(tables: &[TableMetadata], config: &CodegenConfig) -> Result<()> {
51 let output_dir = &config.output_dao_dir;
52 fs::create_dir_all(output_dir)?;
53
54 let mut mod_content = String::new();
56 mod_content.push_str("// Generated DAO functions\n\n");
57
58 for table in tables {
59 let file_name = heck::AsSnakeCase(&table.name).to_string();
60 mod_content.push_str(&format!(
61 "#[allow(dead_code, clippy::all)]\npub mod {};\n",
62 file_name
63 ));
64 }
65
66 let mod_path = output_dir.join("mod.rs");
67 fs::write(&mod_path, &mod_content)?;
68
69 for table in tables {
71 generate_dao_file(table, output_dir, &config.models_module)?;
72 }
73
74 Ok(())
75}
76
77fn generate_dao_file(table: &TableMetadata, output_dir: &Path, models_module: &str) -> Result<()> {
79 let struct_name = to_struct_name(&table.name);
80 let file_name = format!("{}.rs", heck::AsSnakeCase(&table.name));
81 debug!("Generating DAO for {} -> {}", struct_name, file_name);
82
83 let mut code = String::new();
84
85 let column_map: HashMap<&str, &ColumnMetadata> =
87 table.columns.iter().map(|c| (c.name.as_str(), c)).collect();
88
89 let mut needs_chrono = false;
91 let mut needs_decimal = false;
92
93 for col in &table.columns {
94 let rust_type = TypeResolver::resolve(col, &table.name);
95 if rust_type.needs_chrono() {
96 needs_chrono = true;
97 }
98 if rust_type.needs_decimal() {
99 needs_decimal = true;
100 }
101 }
102
103 let has_enums = table.columns.iter().any(|c| c.is_enum());
105
106 code.push_str("use rdbi::{Pool, Query, Result};\n");
108 code.push_str(&format!("use crate::{}::{};\n", models_module, struct_name));
109
110 if has_enums {
111 for col in &table.columns {
113 if col.is_enum() {
114 let enum_name = super::naming::to_enum_name(&table.name, &col.name);
115 code.push_str(&format!("use crate::{}::{};\n", models_module, enum_name));
116 }
117 }
118 }
119
120 if needs_chrono {
121 code.push_str("#[allow(unused_imports)]\n");
122 code.push_str("use chrono::{NaiveDate, NaiveDateTime, NaiveTime};\n");
123 }
124 if needs_decimal {
125 code.push_str("#[allow(unused_imports)]\n");
126 code.push_str("use rust_decimal::Decimal;\n");
127 }
128
129 code.push('\n');
130
131 let select_columns = build_select_columns(table);
133
134 code.push_str(&generate_find_all(table, &struct_name, &select_columns));
136
137 code.push_str(&generate_count_all(table));
139
140 if let Some(pk) = &table.primary_key {
142 code.push_str(&generate_pk_methods(
143 table,
144 pk,
145 &column_map,
146 &struct_name,
147 &select_columns,
148 ));
149 }
150
151 code.push_str(&generate_insert_methods(table, &struct_name, &column_map));
153
154 code.push_str(&generate_insert_plain_method(table, &column_map));
156
157 code.push_str(&generate_insert_all_method(table, &struct_name));
159
160 code.push_str(&generate_upsert_method(table, &struct_name));
162
163 if table.primary_key.is_some() {
165 code.push_str(&generate_update_methods(table, &struct_name, &column_map));
166 code.push_str(&generate_update_plain_method(table, &column_map));
168 }
169
170 let signatures = collect_method_signatures(table);
172 let mut sorted_signatures: Vec<&MethodSignature> = signatures.values().collect();
173 sorted_signatures.sort_by_key(|s| s.method_name.clone());
174
175 for sig in &sorted_signatures {
176 if sig.source == "PRIMARY_KEY" {
178 continue;
179 }
180 code.push_str(&generate_find_by_method(
181 table,
182 sig,
183 &column_map,
184 &struct_name,
185 &select_columns,
186 ));
187 }
188
189 if table.primary_key.is_some() {
191 code.push_str(&generate_set_struct(table, &struct_name));
192 for sig in &sorted_signatures {
193 code.push_str(&generate_partial_update_method(
194 table,
195 sig,
196 &column_map,
197 &struct_name,
198 ));
199 }
200 }
201
202 for sig in &sorted_signatures {
204 if sig.source == "PRIMARY_KEY" {
205 continue;
206 }
207 code.push_str(&generate_delete_by_method(table, sig, &column_map));
208 code.push_str(&generate_count_by_method(table, sig, &column_map));
209 }
210
211 code.push_str(&generate_find_by_list_methods(
213 table,
214 &column_map,
215 &struct_name,
216 &select_columns,
217 ));
218
219 code.push_str(&generate_composite_enum_list_methods(
221 table,
222 &column_map,
223 &struct_name,
224 &select_columns,
225 ));
226
227 code.push_str(&generate_pagination_methods(
229 table,
230 &struct_name,
231 &select_columns,
232 models_module,
233 ));
234
235 let file_path = output_dir.join(&file_name);
236 fs::write(&file_path, &code)?;
237 Ok(())
238}
239
240fn build_select_columns(table: &TableMetadata) -> String {
242 table
243 .columns
244 .iter()
245 .map(|c| format!("`{}`", c.name))
246 .collect::<Vec<_>>()
247 .join(", ")
248}
249
250fn build_where_clause(columns: &[String]) -> String {
252 columns
253 .iter()
254 .map(|c| format!("`{}` = ?", c))
255 .collect::<Vec<_>>()
256 .join(" AND ")
257}
258
259fn build_params(
261 columns: &[String],
262 column_map: &HashMap<&str, &ColumnMetadata>,
263 table_name: &str,
264) -> String {
265 columns
266 .iter()
267 .map(|c| {
268 let col = column_map.get(c.as_str()).unwrap();
269 let rust_type = TypeResolver::resolve(col, table_name);
270 let param_type = rust_type.to_param_type_string();
271 format!("{}: {}", escape_field_name(c), param_type)
272 })
273 .collect::<Vec<_>>()
274 .join(", ")
275}
276
277fn generate_bind_section(columns: &[String]) -> String {
279 columns
280 .iter()
281 .map(|c| format!(".bind({})", escape_field_name(c)))
282 .collect::<Vec<_>>()
283 .join("\n")
284}
285
286fn generate_find_all(table: &TableMetadata, struct_name: &str, select_columns: &str) -> String {
288 format!(
289 r#"/// Find all records
290pub async fn find_all<P: Pool>(pool: &P) -> Result<Vec<{struct_name}>> {{
291Query::new("SELECT {select_columns} FROM `{table_name}`")
292.fetch_all(pool).await
293}}
294"#,
295 struct_name = struct_name,
296 select_columns = select_columns,
297 table_name = table.name,
298 )
299}
300
301fn generate_count_all(table: &TableMetadata) -> String {
303 format!(
304 r#"/// Count all records
305pub async fn count_all<P: Pool>(pool: &P) -> Result<i64> {{
306Query::new("SELECT COUNT(*) FROM `{table_name}`")
307.fetch_scalar(pool).await
308}}
309"#,
310 table_name = table.name,
311 )
312}
313
314fn generate_pk_methods(
316 table: &TableMetadata,
317 pk: &crate::parser::PrimaryKey,
318 column_map: &HashMap<&str, &ColumnMetadata>,
319 struct_name: &str,
320 select_columns: &str,
321) -> String {
322 let mut code = String::new();
323
324 let method_name = generate_find_by_method_name(&pk.columns);
325 let params = build_params(&pk.columns, column_map, &table.name);
326 let where_clause = build_where_clause(&pk.columns);
327 let bind_section = generate_bind_section(&pk.columns);
328
329 code.push_str(&format!(
331 r#"/// Find by primary key
332pub async fn {method_name}<P: Pool>(pool: &P, {params}) -> Result<Option<{struct_name}>> {{
333Query::new("SELECT {select_columns} FROM `{table_name}` WHERE {where_clause}")
334{bind_section}
335.fetch_optional(pool).await
336}}
337"#,
338 method_name = method_name,
339 params = params,
340 struct_name = struct_name,
341 select_columns = select_columns,
342 table_name = table.name,
343 where_clause = where_clause,
344 bind_section = bind_section,
345 ));
346
347 let delete_method = generate_delete_by_method_name(&pk.columns);
349 code.push_str(&format!(
350 r#"/// Delete by primary key
351pub async fn {delete_method}<P: Pool>(pool: &P, {params}) -> Result<u64> {{
352Query::new("DELETE FROM `{table_name}` WHERE {where_clause}")
353{bind_section}
354.execute(pool).await.map(|r| r.rows_affected)
355}}
356"#,
357 delete_method = delete_method,
358 params = params,
359 table_name = table.name,
360 where_clause = where_clause,
361 bind_section = bind_section,
362 ));
363
364 code
365}
366
367fn generate_insert_methods(
369 table: &TableMetadata,
370 struct_name: &str,
371 _column_map: &HashMap<&str, &ColumnMetadata>,
372) -> String {
373 let mut code = String::new();
374
375 let insert_columns: Vec<&ColumnMetadata> = table
377 .columns
378 .iter()
379 .filter(|c| !c.is_auto_increment)
380 .collect();
381
382 if insert_columns.is_empty() {
383 return code;
384 }
385
386 let column_list = insert_columns
387 .iter()
388 .map(|c| format!("`{}`", c.name))
389 .collect::<Vec<_>>()
390 .join(", ");
391
392 let placeholders = insert_columns
393 .iter()
394 .map(|_| "?")
395 .collect::<Vec<_>>()
396 .join(", ");
397
398 let bind_fields = insert_columns
399 .iter()
400 .map(|c| {
401 let field = escape_field_name(&c.name);
402 let rust_type = TypeResolver::resolve(c, &table.name);
403 if rust_type.is_copy() {
404 format!(".bind(entity.{})", field)
405 } else {
406 format!(".bind(&entity.{})", field)
407 }
408 })
409 .collect::<Vec<_>>()
410 .join("\n");
411
412 code.push_str(&format!(
414 r#"/// Insert a new record
415pub async fn insert<P: Pool>(pool: &P, entity: &{struct_name}) -> Result<u64> {{
416Query::new("INSERT INTO `{table_name}` ({column_list}) VALUES ({placeholders})")
417{bind_fields}
418.execute(pool).await.map(|r| r.last_insert_id.unwrap_or(0))
419}}
420"#,
421 struct_name = struct_name,
422 table_name = table.name,
423 column_list = column_list,
424 placeholders = placeholders,
425 bind_fields = bind_fields,
426 ));
427
428 code
429}
430
431fn generate_insert_plain_method(
433 table: &TableMetadata,
434 column_map: &HashMap<&str, &ColumnMetadata>,
435) -> String {
436 let insert_columns: Vec<&ColumnMetadata> = table
438 .columns
439 .iter()
440 .filter(|c| !c.is_auto_increment)
441 .collect();
442
443 if insert_columns.is_empty() {
444 return String::new();
445 }
446
447 let column_names: Vec<String> = insert_columns.iter().map(|c| c.name.clone()).collect();
448 let params = build_params(&column_names, column_map, &table.name);
449
450 let column_list = insert_columns
451 .iter()
452 .map(|c| format!("`{}`", c.name))
453 .collect::<Vec<_>>()
454 .join(", ");
455
456 let placeholders = insert_columns
457 .iter()
458 .map(|_| "?")
459 .collect::<Vec<_>>()
460 .join(", ");
461
462 let bind_section = generate_bind_section(&column_names);
463
464 format!(
465 r#"/// Insert a new record with individual parameters
466#[allow(clippy::too_many_arguments)]
467pub async fn insert_plain<P: Pool>(pool: &P, {params}) -> Result<u64> {{
468Query::new("INSERT INTO `{table_name}` ({column_list}) VALUES ({placeholders})")
469{bind_section}
470.execute(pool).await.map(|r| r.last_insert_id.unwrap_or(0))
471}}
472"#,
473 params = params,
474 table_name = table.name,
475 column_list = column_list,
476 placeholders = placeholders,
477 bind_section = bind_section,
478 )
479}
480
481fn generate_insert_all_method(table: &TableMetadata, struct_name: &str) -> String {
483 let insert_columns: Vec<&ColumnMetadata> = table
485 .columns
486 .iter()
487 .filter(|c| !c.is_auto_increment)
488 .collect();
489
490 if insert_columns.is_empty() {
491 return String::new();
492 }
493
494 format!(
495 r#"/// Insert multiple records in a single batch
496pub async fn insert_all<P: Pool>(pool: &P, entities: &[{struct_name}]) -> Result<u64> {{
497rdbi::BatchInsert::new("{table_name}", entities)
498.execute(pool).await.map(|r| r.rows_affected)
499}}
500"#,
501 struct_name = struct_name,
502 table_name = table.name,
503 )
504}
505
506fn has_unique_index(table: &TableMetadata) -> bool {
508 table.indexes.iter().any(|idx| idx.unique)
509}
510
511fn generate_upsert_method(table: &TableMetadata, struct_name: &str) -> String {
513 if table.primary_key.is_none() && !has_unique_index(table) {
515 return String::new();
516 }
517
518 let insert_columns: Vec<&ColumnMetadata> = table
520 .columns
521 .iter()
522 .filter(|c| !c.is_auto_increment)
523 .collect();
524
525 if insert_columns.is_empty() {
526 return String::new();
527 }
528
529 let pk_columns: HashSet<&str> = table
531 .primary_key
532 .as_ref()
533 .map(|pk| pk.columns.iter().map(|s| s.as_str()).collect())
534 .unwrap_or_default();
535
536 let update_columns: Vec<&ColumnMetadata> = insert_columns
538 .iter()
539 .filter(|c| !pk_columns.contains(c.name.as_str()))
540 .copied()
541 .collect();
542
543 if update_columns.is_empty() {
545 return String::new();
546 }
547
548 let column_list = insert_columns
549 .iter()
550 .map(|c| format!("`{}`", c.name))
551 .collect::<Vec<_>>()
552 .join(", ");
553
554 let placeholders = insert_columns
555 .iter()
556 .map(|_| "?")
557 .collect::<Vec<_>>()
558 .join(", ");
559
560 let update_clause = update_columns
561 .iter()
562 .map(|c| format!("`{name}` = VALUES(`{name}`)", name = c.name))
563 .collect::<Vec<_>>()
564 .join(", ");
565
566 let bind_fields = insert_columns
567 .iter()
568 .map(|c| {
569 let field = escape_field_name(&c.name);
570 let rust_type = TypeResolver::resolve(c, &table.name);
571 if rust_type.is_copy() {
572 format!(".bind(entity.{})", field)
573 } else {
574 format!(".bind(&entity.{})", field)
575 }
576 })
577 .collect::<Vec<_>>()
578 .join("\n");
579
580 format!(
581 r#"/// Upsert a record (insert or update on duplicate key)
582/// Returns rows_affected: 1 if inserted, 2 if updated
583pub async fn upsert<P: Pool>(pool: &P, entity: &{struct_name}) -> Result<u64> {{
584Query::new("INSERT INTO `{table_name}` ({column_list}) VALUES ({placeholders}) ON DUPLICATE KEY UPDATE {update_clause}")
585{bind_fields}
586.execute(pool).await.map(|r| r.rows_affected)
587}}
588"#,
589 struct_name = struct_name,
590 table_name = table.name,
591 column_list = column_list,
592 placeholders = placeholders,
593 update_clause = update_clause,
594 bind_fields = bind_fields,
595 )
596}
597
598fn generate_update_methods(
600 table: &TableMetadata,
601 struct_name: &str,
602 column_map: &HashMap<&str, &ColumnMetadata>,
603) -> String {
604 let mut code = String::new();
605
606 let pk = table.primary_key.as_ref().unwrap();
607
608 let update_columns: Vec<&ColumnMetadata> = table
610 .columns
611 .iter()
612 .filter(|c| !pk.columns.contains(&c.name))
613 .collect();
614
615 if update_columns.is_empty() {
616 return code;
617 }
618
619 let set_clause = update_columns
620 .iter()
621 .map(|c| format!("`{}` = ?", c.name))
622 .collect::<Vec<_>>()
623 .join(", ");
624
625 let where_clause = build_where_clause(&pk.columns);
626
627 let bind_fields: Vec<String> = update_columns
629 .iter()
630 .map(|c| {
631 let field = escape_field_name(&c.name);
632 let rust_type = TypeResolver::resolve(c, &table.name);
633 if rust_type.is_copy() {
634 format!(".bind(entity.{})", field)
635 } else {
636 format!(".bind(&entity.{})", field)
637 }
638 })
639 .chain(pk.columns.iter().map(|c| {
640 let field = escape_field_name(c);
641 let col = column_map.get(c.as_str()).unwrap();
642 let rust_type = TypeResolver::resolve(col, &table.name);
643 if rust_type.is_copy() {
644 format!(".bind(entity.{})", field)
645 } else {
646 format!(".bind(&entity.{})", field)
647 }
648 }))
649 .collect();
650
651 code.push_str(&format!(
652 r#"/// Update a record by primary key
653pub async fn update<P: Pool>(pool: &P, entity: &{struct_name}) -> Result<u64> {{
654Query::new("UPDATE `{table_name}` SET {set_clause} WHERE {where_clause}")
655{bind_fields}
656.execute(pool).await.map(|r| r.rows_affected)
657}}
658"#,
659 struct_name = struct_name,
660 table_name = table.name,
661 set_clause = set_clause,
662 where_clause = where_clause,
663 bind_fields = bind_fields.join("\n"),
664 ));
665
666 code
667}
668
669fn generate_update_plain_method(
671 table: &TableMetadata,
672 column_map: &HashMap<&str, &ColumnMetadata>,
673) -> String {
674 let pk = table.primary_key.as_ref().unwrap();
675
676 let update_columns: Vec<&ColumnMetadata> = table
678 .columns
679 .iter()
680 .filter(|c| !pk.columns.contains(&c.name))
681 .collect();
682
683 if update_columns.is_empty() {
684 return String::new();
685 }
686
687 let method_name = generate_update_by_method_name(&pk.columns);
689
690 let pk_params = build_params(&pk.columns, column_map, &table.name);
692 let update_column_names: Vec<String> = update_columns.iter().map(|c| c.name.clone()).collect();
693 let update_params = build_params(&update_column_names, column_map, &table.name);
694 let all_params = format!("{}, {}", pk_params, update_params);
695
696 let set_clause = update_columns
697 .iter()
698 .map(|c| format!("`{}` = ?", c.name))
699 .collect::<Vec<_>>()
700 .join(", ");
701
702 let where_clause = build_where_clause(&pk.columns);
703
704 let bind_section_update = generate_bind_section(&update_column_names);
706 let bind_section_pk = generate_bind_section(&pk.columns);
707
708 format!(
709 r#"/// Update a record by primary key with individual parameters
710#[allow(clippy::too_many_arguments)]
711pub async fn {method_name}<P: Pool>(pool: &P, {all_params}) -> Result<u64> {{
712Query::new("UPDATE `{table_name}` SET {set_clause} WHERE {where_clause}")
713{bind_section_update}
714{bind_section_pk}
715.execute(pool).await.map(|r| r.rows_affected)
716}}
717"#,
718 method_name = method_name,
719 all_params = all_params,
720 table_name = table.name,
721 set_clause = set_clause,
722 where_clause = where_clause,
723 bind_section_update = bind_section_update,
724 bind_section_pk = bind_section_pk,
725 )
726}
727
728fn generate_set_struct(table: &TableMetadata, struct_name: &str) -> String {
731 let pk = table.primary_key.as_ref().unwrap();
732 let update_columns: Vec<&ColumnMetadata> = table
733 .columns
734 .iter()
735 .filter(|c| !pk.columns.contains(&c.name))
736 .collect();
737
738 if update_columns.is_empty() {
739 return String::new();
740 }
741
742 let fields: Vec<String> = update_columns
743 .iter()
744 .map(|c| {
745 let rust_type = TypeResolver::resolve(c, &table.name);
746 let inner = rust_type.inner_type().to_type_string();
747 format!(
748 "/// Settable column `{}` (None = leave unchanged)\npub {}: Option<{}>,",
749 c.name,
750 escape_field_name(&c.name),
751 inner
752 )
753 })
754 .collect();
755
756 format!(
757 r#"/// Partial-update column set for `{table_name}`. Only `Some` fields are written by the
758/// `update_partial_by_*` methods; nullable columns cannot be set to NULL this way.
759#[derive(Debug, Default, Clone)]
760pub struct Set{struct_name} {{
761{fields}
762}}
763"#,
764 table_name = table.name,
765 struct_name = struct_name,
766 fields = fields.join("\n"),
767 )
768}
769
770fn build_dynamic_where_code(
773 columns: &[String],
774 column_map: &HashMap<&str, &ColumnMetadata>,
775) -> (String, String) {
776 let mut pushes = Vec::new();
777 let mut binds = Vec::new();
778 for col in columns {
779 let col_meta = column_map.get(col.as_str()).unwrap();
780 let field = escape_field_name(col);
781 if col_meta.nullable {
782 pushes.push(format!(
783 "if {field}.is_some() {{ wheres.push(\"`{col}` = ?\".to_string()); }} else {{ wheres.push(\"`{col}` IS NULL\".to_string()); }}",
784 ));
785 binds.push(format!(
786 "if let Some(v) = {field}.as_ref() {{ query = query.bind(v); }}"
787 ));
788 } else {
789 pushes.push(format!("wheres.push(\"`{col}` = ?\".to_string());"));
790 binds.push(format!("query = query.bind({field});"));
791 }
792 }
793 (
794 format!(
795 "let mut wheres: Vec<String> = Vec::new();\n{}",
796 pushes.join("\n")
797 ),
798 binds.join("\n"),
799 )
800}
801
802fn generate_partial_update_method(
804 table: &TableMetadata,
805 sig: &MethodSignature,
806 column_map: &HashMap<&str, &ColumnMetadata>,
807 struct_name: &str,
808) -> String {
809 let pk = table.primary_key.as_ref().unwrap();
810 let update_columns: Vec<&ColumnMetadata> = table
811 .columns
812 .iter()
813 .filter(|c| !pk.columns.contains(&c.name))
814 .collect();
815
816 if update_columns.is_empty() {
817 return String::new();
818 }
819
820 let method_name = generate_update_partial_by_method_name(&sig.columns);
821 let where_params = build_params(&sig.columns, column_map, &table.name);
822
823 let set_pushes: Vec<String> = update_columns
824 .iter()
825 .map(|c| {
826 format!(
827 "if set.{field}.is_some() {{ sets.push(\"`{col}` = ?\"); }}",
828 field = escape_field_name(&c.name),
829 col = c.name
830 )
831 })
832 .collect();
833 let set_binds: Vec<String> = update_columns
834 .iter()
835 .map(|c| {
836 format!(
837 "if let Some(v) = set.{field}.as_ref() {{ query = query.bind(v); }}",
838 field = escape_field_name(&c.name)
839 )
840 })
841 .collect();
842
843 let has_nullable = sig.columns.iter().any(|c| {
844 column_map
845 .get(c.as_str())
846 .map(|col| col.nullable)
847 .unwrap_or(false)
848 });
849
850 let body = if has_nullable {
851 let (where_setup, where_binds) = build_dynamic_where_code(&sig.columns, column_map);
852 format!(
853 r#"let mut sets: Vec<&str> = Vec::new();
854{set_pushes}
855if sets.is_empty() {{
856return Ok(0);
857}}
858{where_setup}
859let sql = format!("UPDATE `{table}` SET {{}} WHERE {{}}", sets.join(", "), wheres.join(" AND "));
860let mut query = rdbi::DynamicQuery::new(sql);
861{set_binds}
862{where_binds}
863query.execute(pool).await.map(|r| r.rows_affected)"#,
864 set_pushes = set_pushes.join("\n"),
865 where_setup = where_setup,
866 table = table.name,
867 set_binds = set_binds.join("\n"),
868 where_binds = where_binds,
869 )
870 } else {
871 let where_clause = build_where_clause(&sig.columns);
872 let where_binds: Vec<String> = sig
873 .columns
874 .iter()
875 .map(|c| format!("query = query.bind({});", escape_field_name(c)))
876 .collect();
877 format!(
878 r#"let mut sets: Vec<&str> = Vec::new();
879{set_pushes}
880if sets.is_empty() {{
881return Ok(0);
882}}
883let sql = format!("UPDATE `{table}` SET {{}} WHERE {where_clause}", sets.join(", "));
884let mut query = rdbi::DynamicQuery::new(sql);
885{set_binds}
886{where_binds}
887query.execute(pool).await.map(|r| r.rows_affected)"#,
888 set_pushes = set_pushes.join("\n"),
889 table = table.name,
890 where_clause = where_clause,
891 set_binds = set_binds.join("\n"),
892 where_binds = where_binds.join("\n"),
893 )
894 };
895
896 format!(
897 r#"/// Partial update of `{table_name}` by {source}: writes only the `Some` fields of `set`.
898/// An all-`None` `set` is a no-op returning 0; nullable columns cannot be set to NULL.
899pub async fn {method_name}<P: Pool>(pool: &P, {where_params}, set: Set{struct_name}) -> Result<u64> {{
900{body}
901}}
902"#,
903 table_name = table.name,
904 source = sig.source.to_lowercase().replace('_', " "),
905 method_name = method_name,
906 where_params = where_params,
907 struct_name = struct_name,
908 body = body,
909 )
910}
911
912fn generate_delete_by_method(
914 table: &TableMetadata,
915 sig: &MethodSignature,
916 column_map: &HashMap<&str, &ColumnMetadata>,
917) -> String {
918 let method_name = generate_delete_by_method_name(&sig.columns);
919 let params = build_params(&sig.columns, column_map, &table.name);
920
921 let has_nullable = sig.columns.iter().any(|c| {
922 column_map
923 .get(c.as_str())
924 .map(|col| col.nullable)
925 .unwrap_or(false)
926 });
927
928 if has_nullable {
929 let (where_setup, where_binds) = build_dynamic_where_code(&sig.columns, column_map);
930 format!(
931 r#"/// Delete rows matched by {source} columns; NULL parameters match `IS NULL`.
932pub async fn {method_name}<P: Pool>(pool: &P, {params}) -> Result<u64> {{
933{where_setup}
934let sql = format!("DELETE FROM `{table_name}` WHERE {{}}", wheres.join(" AND "));
935let mut query = rdbi::DynamicQuery::new(sql);
936{where_binds}
937query.execute(pool).await.map(|r| r.rows_affected)
938}}
939"#,
940 source = sig.source.to_lowercase().replace('_', " "),
941 method_name = method_name,
942 params = params,
943 where_setup = where_setup,
944 table_name = table.name,
945 where_binds = where_binds,
946 )
947 } else {
948 let where_clause = build_where_clause(&sig.columns);
949 let bind_section = generate_bind_section(&sig.columns);
950 format!(
951 r#"/// Delete rows matched by {source} columns.
952pub async fn {method_name}<P: Pool>(pool: &P, {params}) -> Result<u64> {{
953Query::new("DELETE FROM `{table_name}` WHERE {where_clause}")
954{bind_section}
955.execute(pool).await.map(|r| r.rows_affected)
956}}
957"#,
958 source = sig.source.to_lowercase().replace('_', " "),
959 method_name = method_name,
960 params = params,
961 table_name = table.name,
962 where_clause = where_clause,
963 bind_section = bind_section,
964 )
965 }
966}
967
968fn generate_count_by_method(
970 table: &TableMetadata,
971 sig: &MethodSignature,
972 column_map: &HashMap<&str, &ColumnMetadata>,
973) -> String {
974 let method_name = generate_count_by_method_name(&sig.columns);
975 let params = build_params(&sig.columns, column_map, &table.name);
976
977 let has_nullable = sig.columns.iter().any(|c| {
978 column_map
979 .get(c.as_str())
980 .map(|col| col.nullable)
981 .unwrap_or(false)
982 });
983
984 if has_nullable {
985 let (where_setup, where_binds) = build_dynamic_where_code(&sig.columns, column_map);
986 format!(
987 r#"/// Count rows matched by {source} columns; NULL parameters match `IS NULL`.
988pub async fn {method_name}<P: Pool>(pool: &P, {params}) -> Result<i64> {{
989{where_setup}
990let sql = format!("SELECT COUNT(*) FROM `{table_name}` WHERE {{}}", wheres.join(" AND "));
991let mut query = rdbi::DynamicQuery::new(sql);
992{where_binds}
993query.fetch_scalar(pool).await
994}}
995"#,
996 source = sig.source.to_lowercase().replace('_', " "),
997 method_name = method_name,
998 params = params,
999 where_setup = where_setup,
1000 table_name = table.name,
1001 where_binds = where_binds,
1002 )
1003 } else {
1004 let where_clause = build_where_clause(&sig.columns);
1005 let bind_section = generate_bind_section(&sig.columns);
1006 format!(
1007 r#"/// Count rows matched by {source} columns.
1008pub async fn {method_name}<P: Pool>(pool: &P, {params}) -> Result<i64> {{
1009Query::new("SELECT COUNT(*) FROM `{table_name}` WHERE {where_clause}")
1010{bind_section}
1011.fetch_scalar(pool).await
1012}}
1013"#,
1014 source = sig.source.to_lowercase().replace('_', " "),
1015 method_name = method_name,
1016 params = params,
1017 table_name = table.name,
1018 where_clause = where_clause,
1019 bind_section = bind_section,
1020 )
1021 }
1022}
1023
1024fn collect_method_signatures(table: &TableMetadata) -> HashMap<Vec<String>, MethodSignature> {
1026 let mut signatures: HashMap<Vec<String>, MethodSignature> = HashMap::new();
1027
1028 if let Some(pk) = &table.primary_key {
1030 let sig = MethodSignature::new(
1031 pk.columns.clone(),
1032 PRIORITY_PRIMARY_KEY,
1033 true,
1034 "PRIMARY_KEY",
1035 );
1036 signatures.insert(pk.columns.clone(), sig);
1037 }
1038
1039 for index in &table.indexes {
1041 let priority = if index.unique {
1042 PRIORITY_UNIQUE_INDEX
1043 } else {
1044 PRIORITY_NON_UNIQUE_INDEX
1045 };
1046 let source = if index.unique {
1047 "UNIQUE_INDEX"
1048 } else {
1049 "NON_UNIQUE_INDEX"
1050 };
1051 let sig = MethodSignature::new(index.columns.clone(), priority, index.unique, source);
1052
1053 if let Some(existing) = signatures.get(&index.columns) {
1055 if sig.priority < existing.priority {
1056 signatures.insert(index.columns.clone(), sig);
1057 }
1058 } else {
1059 signatures.insert(index.columns.clone(), sig);
1060 }
1061 }
1062
1063 for fk in &table.foreign_keys {
1065 let columns = vec![fk.column_name.clone()];
1066 let sig = MethodSignature::new(columns.clone(), PRIORITY_FOREIGN_KEY, false, "FOREIGN_KEY");
1067
1068 signatures.entry(columns).or_insert(sig);
1069 }
1070
1071 signatures
1072}
1073
1074fn generate_find_by_method(
1076 table: &TableMetadata,
1077 sig: &MethodSignature,
1078 column_map: &HashMap<&str, &ColumnMetadata>,
1079 struct_name: &str,
1080 select_columns: &str,
1081) -> String {
1082 let params = build_params(&sig.columns, column_map, &table.name);
1083
1084 let (return_type, fetch_method) = if sig.is_unique {
1085 (format!("Option<{}>", struct_name), "fetch_optional")
1086 } else {
1087 (format!("Vec<{}>", struct_name), "fetch_all")
1088 };
1089
1090 let return_desc = if sig.is_unique {
1091 "Option (unique)"
1092 } else {
1093 "Vec (non-unique)"
1094 };
1095
1096 let has_nullable = sig.columns.iter().any(|c| {
1098 column_map
1099 .get(c.as_str())
1100 .map(|col| col.nullable)
1101 .unwrap_or(false)
1102 });
1103
1104 if has_nullable {
1105 generate_find_by_method_nullable(
1107 table,
1108 sig,
1109 column_map,
1110 select_columns,
1111 ¶ms,
1112 &return_type,
1113 fetch_method,
1114 return_desc,
1115 )
1116 } else {
1117 let where_clause = build_where_clause(&sig.columns);
1119 let bind_section = generate_bind_section(&sig.columns);
1120
1121 format!(
1122 r#"/// Find by {source}: returns {return_desc}
1123pub async fn {method_name}<P: Pool>(pool: &P, {params}) -> Result<{return_type}> {{
1124Query::new("SELECT {select_columns} FROM `{table_name}` WHERE {where_clause}")
1125{bind_section}
1126.{fetch_method}(pool).await
1127}}
1128"#,
1129 source = sig.source.to_lowercase().replace('_', " "),
1130 return_desc = return_desc,
1131 method_name = sig.method_name,
1132 params = params,
1133 return_type = return_type,
1134 select_columns = select_columns,
1135 table_name = table.name,
1136 where_clause = where_clause,
1137 bind_section = bind_section,
1138 fetch_method = fetch_method,
1139 )
1140 }
1141}
1142
1143#[allow(clippy::too_many_arguments)]
1145fn generate_find_by_method_nullable(
1146 table: &TableMetadata,
1147 sig: &MethodSignature,
1148 column_map: &HashMap<&str, &ColumnMetadata>,
1149 select_columns: &str,
1150 params: &str,
1151 return_type: &str,
1152 fetch_method: &str,
1153 return_desc: &str,
1154) -> String {
1155 let mut where_parts = Vec::new();
1157 let mut bind_parts = Vec::new();
1158
1159 for col in &sig.columns {
1160 let col_meta = column_map.get(col.as_str()).unwrap();
1161 let field_name = escape_field_name(col);
1162
1163 if col_meta.nullable {
1164 where_parts.push(format!(
1166 r#"if {field}.is_some() {{ "`{col}` = ?" }} else {{ "`{col}` IS NULL" }}"#,
1167 field = field_name,
1168 col = col,
1169 ));
1170 bind_parts.push(format!(
1171 r#"if let Some(v) = {field}.as_ref() {{ query = query.bind(v); }}"#,
1172 field = field_name,
1173 ));
1174 } else {
1175 where_parts.push(format!(r#""`{}` = ?""#, col));
1176 bind_parts.push(format!(r#"query = query.bind({});"#, field_name,));
1177 }
1178 }
1179
1180 let where_expr = if where_parts.len() == 1 {
1181 where_parts[0].clone()
1182 } else {
1183 let parts = where_parts
1185 .iter()
1186 .map(|p| format!("({})", p))
1187 .collect::<Vec<_>>()
1188 .join(", ");
1189 format!("vec![{}].join(\" AND \")", parts)
1190 };
1191
1192 let bind_code = bind_parts.join("\n");
1193
1194 format!(
1195 r#"/// Find by {source}: returns {return_desc}
1196pub async fn {method_name}<P: Pool>(pool: &P, {params}) -> Result<{return_type}> {{
1197let where_clause = {where_expr};
1198let sql = format!("SELECT {select_columns} FROM `{table_name}` WHERE {{}}", where_clause);
1199let mut query = rdbi::DynamicQuery::new(sql);
1200{bind_code}
1201query.{fetch_method}(pool).await
1202}}
1203"#,
1204 source = sig.source.to_lowercase().replace('_', " "),
1205 return_desc = return_desc,
1206 method_name = sig.method_name,
1207 params = params,
1208 return_type = return_type,
1209 select_columns = select_columns,
1210 table_name = table.name,
1211 where_expr = where_expr,
1212 bind_code = bind_code,
1213 fetch_method = fetch_method,
1214 )
1215}
1216
1217fn generate_find_by_list_methods(
1219 table: &TableMetadata,
1220 column_map: &HashMap<&str, &ColumnMetadata>,
1221 struct_name: &str,
1222 select_columns: &str,
1223) -> String {
1224 let mut code = String::new();
1225 let mut processed: HashSet<String> = HashSet::new();
1226
1227 if let Some(pk) = &table.primary_key {
1229 if pk.columns.len() == 1 {
1230 let col = &pk.columns[0];
1231 code.push_str(&generate_single_find_by_list(
1232 table,
1233 col,
1234 column_map,
1235 struct_name,
1236 select_columns,
1237 ));
1238 processed.insert(col.clone());
1239 }
1240 }
1241
1242 for index in &table.indexes {
1244 if index.columns.len() == 1 {
1245 let col = &index.columns[0];
1246 if !processed.contains(col) {
1247 code.push_str(&generate_single_find_by_list(
1248 table,
1249 col,
1250 column_map,
1251 struct_name,
1252 select_columns,
1253 ));
1254 processed.insert(col.clone());
1255 }
1256 }
1257 }
1258
1259 code
1260}
1261
1262fn generate_single_find_by_list(
1264 table: &TableMetadata,
1265 column_name: &str,
1266 column_map: &HashMap<&str, &ColumnMetadata>,
1267 struct_name: &str,
1268 select_columns: &str,
1269) -> String {
1270 let method_name = generate_find_by_list_method_name(column_name);
1271 let param_name = pluralize(&escape_field_name(column_name));
1272 let column = column_map.get(column_name).unwrap();
1273 let rust_type = TypeResolver::resolve(column, &table.name);
1274
1275 let inner_type = rust_type.inner_type().to_type_string();
1277
1278 let column_name_plural = pluralize(column_name);
1279 format!(
1280 r#"/// Find by list of {column_name_plural} (IN clause)
1281pub async fn {method_name}<P: Pool>(pool: &P, {param_name}: &[{inner_type}]) -> Result<Vec<{struct_name}>> {{
1282if {param_name}.is_empty() {{
1283return Ok(Vec::new());
1284}}
1285let placeholders = {param_name}.iter().map(|_| "?").collect::<Vec<_>>().join(",");
1286let query = format!("SELECT {select_columns} FROM `{table_name}` WHERE `{column_name}` IN ({{}})", placeholders);
1287rdbi::DynamicQuery::new(query)
1288.bind_all({param_name})
1289.fetch_all(pool).await
1290}}
1291"#,
1292 column_name_plural = column_name_plural,
1293 column_name = column_name,
1294 method_name = method_name,
1295 param_name = param_name,
1296 inner_type = inner_type,
1297 struct_name = struct_name,
1298 select_columns = select_columns,
1299 table_name = table.name,
1300 )
1301}
1302
1303fn generate_composite_enum_list_methods(
1306 table: &TableMetadata,
1307 column_map: &HashMap<&str, &ColumnMetadata>,
1308 struct_name: &str,
1309 select_columns: &str,
1310) -> String {
1311 let mut code = String::new();
1312
1313 for index in &table.indexes {
1314 if index.columns.len() <= 1 {
1316 continue;
1317 }
1318
1319 let enum_columns: HashSet<&str> = index
1321 .columns
1322 .iter()
1323 .filter(|col_name| {
1324 column_map
1325 .get(col_name.as_str())
1326 .map(|col| col.is_enum())
1327 .unwrap_or(false)
1328 })
1329 .map(|s| s.as_str())
1330 .collect();
1331
1332 if enum_columns.is_empty() {
1334 continue;
1335 }
1336
1337 let first_column = &index.columns[0];
1339 if enum_columns.contains(first_column.as_str()) {
1340 continue;
1341 }
1342
1343 code.push_str(&generate_composite_enum_list_method(
1344 table,
1345 &index.columns,
1346 &enum_columns,
1347 column_map,
1348 struct_name,
1349 select_columns,
1350 ));
1351 }
1352
1353 code
1354}
1355
1356fn generate_composite_enum_list_method(
1358 table: &TableMetadata,
1359 columns: &[String],
1360 enum_columns: &HashSet<&str>,
1361 column_map: &HashMap<&str, &ColumnMetadata>,
1362 struct_name: &str,
1363 select_columns: &str,
1364) -> String {
1365 let method_name = generate_composite_enum_method_name(columns, enum_columns);
1367
1368 let mut params_parts = Vec::new();
1370
1371 for col_name in columns {
1372 let col = column_map.get(col_name.as_str()).unwrap();
1373 let rust_type = TypeResolver::resolve(col, &table.name);
1374 let is_enum = enum_columns.contains(col_name.as_str());
1375
1376 if is_enum {
1377 let param_name = pluralize(&escape_field_name(col_name));
1379 let inner_type = rust_type.inner_type().to_type_string();
1380 params_parts.push(format!("{}: &[{}]", param_name, inner_type));
1381 } else {
1382 let param_name = escape_field_name(col_name);
1384 let param_type = rust_type.to_param_type_string();
1385 params_parts.push(format!("{}: {}", param_name, param_type));
1386 }
1387 }
1388
1389 let params = params_parts.join(", ");
1390
1391 let where_clause_static: Vec<String> = columns
1393 .iter()
1394 .map(|col_name| {
1395 if enum_columns.contains(col_name.as_str()) {
1396 format!("`{}` IN ({{}})", col_name) } else {
1398 format!("`{}` = ?", col_name)
1399 }
1400 })
1401 .collect();
1402
1403 let mut bind_code = String::new();
1405
1406 for col_name in columns {
1408 if !enum_columns.contains(col_name.as_str()) {
1409 let param_name = escape_field_name(col_name);
1410 bind_code.push_str(&format!(".bind({})\n", param_name));
1411 }
1412 }
1413
1414 for col_name in columns {
1416 if enum_columns.contains(col_name.as_str()) {
1417 let param_name = pluralize(&escape_field_name(col_name));
1418 bind_code.push_str(&format!(".bind_all({})\n", param_name));
1419 }
1420 }
1421
1422 let column_desc: Vec<String> = columns
1424 .iter()
1425 .map(|col| {
1426 if enum_columns.contains(col.as_str()) {
1427 pluralize(col)
1428 } else {
1429 col.clone()
1430 }
1431 })
1432 .collect();
1433
1434 let enum_col_names: Vec<&str> = columns
1436 .iter()
1437 .filter(|c| enum_columns.contains(c.as_str()))
1438 .map(|s| s.as_str())
1439 .collect();
1440
1441 let in_clause_builders: Vec<String> = enum_col_names
1443 .iter()
1444 .map(|col| {
1445 let param_name = pluralize(&escape_field_name(col));
1446 format!(
1447 "{param_name}.iter().map(|_| \"?\").collect::<Vec<_>>().join(\",\")",
1448 param_name = param_name
1449 )
1450 })
1451 .collect();
1452
1453 let format_args = in_clause_builders.join(", ");
1455
1456 format!(
1457 r#"/// Find by {column_desc} (composite index with IN clause for enum columns)
1458pub async fn {method_name}<P: Pool>(pool: &P, {params}) -> Result<Vec<{struct_name}>> {{
1459{empty_checks}
1460let where_clause = format!("{where_template}", {format_args});
1461let query = format!("SELECT {select_columns} FROM `{table_name}` WHERE {{}}", where_clause);
1462rdbi::DynamicQuery::new(query)
1463{bind_code}.fetch_all(pool).await
1464}}
1465"#,
1466 column_desc = column_desc.join(" and "),
1467 method_name = method_name,
1468 params = params,
1469 struct_name = struct_name,
1470 select_columns = select_columns,
1471 table_name = table.name,
1472 where_template = where_clause_static.join(" AND "),
1473 format_args = format_args,
1474 bind_code = bind_code,
1475 empty_checks = generate_empty_checks(columns, enum_columns),
1476 )
1477}
1478
1479fn generate_empty_checks(columns: &[String], enum_columns: &HashSet<&str>) -> String {
1481 let mut checks = String::new();
1482 for col_name in columns {
1483 if enum_columns.contains(col_name.as_str()) {
1484 let param_name = pluralize(&escape_field_name(col_name));
1485 checks.push_str(&format!(
1486 "if {}.is_empty() {{ return Ok(Vec::new()); }}\n",
1487 param_name
1488 ));
1489 }
1490 }
1491 checks
1492}
1493
1494fn generate_composite_enum_method_name(columns: &[String], enum_columns: &HashSet<&str>) -> String {
1497 let mut parts = Vec::new();
1498 for col in columns {
1499 if enum_columns.contains(col.as_str()) {
1500 parts.push(pluralize(col));
1501 } else {
1502 parts.push(col.clone());
1503 }
1504 }
1505 generate_find_by_method_name(&parts)
1506}
1507
1508fn generate_pagination_methods(
1510 table: &TableMetadata,
1511 struct_name: &str,
1512 select_columns: &str,
1513 models_module: &str,
1514) -> String {
1515 let sort_by_enum = format!("{}SortBy", struct_name);
1516
1517 format!(
1518 r#"/// Find all records with pagination and sorting
1519pub async fn find_all_paginated<P: Pool>(pool: &P, limit: i32, offset: i32, sort_by: crate::{models_module}::{sort_by_enum}, sort_dir: crate::{models_module}::SortDirection) -> Result<Vec<{struct_name}>> {{
1520let order_clause = format!("{{}} {{}}", sort_by.as_sql(), sort_dir.as_sql());
1521let query = format!("SELECT {select_columns} FROM `{table_name}` ORDER BY {{}} LIMIT ? OFFSET ?", order_clause);
1522rdbi::DynamicQuery::new(query).bind(limit).bind(offset).fetch_all(pool).await
1523}}
1524
1525/// Get paginated result with total count
1526pub async fn get_paginated_result<P: Pool>(pool: &P, page_size: i32, current_page: i32, sort_by: crate::{models_module}::{sort_by_enum}, sort_dir: crate::{models_module}::SortDirection) -> Result<crate::{models_module}::PaginatedResult<{struct_name}>> {{
1527let page_size = page_size.max(1);
1528let current_page = current_page.max(1);
1529let offset = (current_page - 1) * page_size;
1530let total_count = count_all(pool).await?;
1531let items = find_all_paginated(pool, page_size, offset, sort_by, sort_dir).await?;
1532Ok(crate::{models_module}::PaginatedResult::new(items, total_count, current_page, page_size))
1533}}
1534"#,
1535 struct_name = struct_name,
1536 select_columns = select_columns,
1537 table_name = table.name,
1538 models_module = models_module,
1539 sort_by_enum = sort_by_enum,
1540 )
1541}
1542
1543#[cfg(test)]
1544mod tests {
1545 use super::*;
1546 use crate::parser::{IndexMetadata, PrimaryKey};
1547
1548 fn make_table() -> TableMetadata {
1549 TableMetadata {
1550 name: "users".to_string(),
1551 comment: None,
1552 columns: vec![
1553 ColumnMetadata {
1554 name: "id".to_string(),
1555 data_type: "BIGINT".to_string(),
1556 nullable: false,
1557 default_value: None,
1558 is_auto_increment: true,
1559 is_unsigned: false,
1560 enum_values: None,
1561 comment: None,
1562 },
1563 ColumnMetadata {
1564 name: "email".to_string(),
1565 data_type: "VARCHAR(255)".to_string(),
1566 nullable: false,
1567 default_value: None,
1568 is_auto_increment: false,
1569 is_unsigned: false,
1570 enum_values: None,
1571 comment: None,
1572 },
1573 ColumnMetadata {
1574 name: "status".to_string(),
1575 data_type: "ENUM".to_string(),
1576 nullable: false,
1577 default_value: None,
1578 is_auto_increment: false,
1579 is_unsigned: false,
1580 enum_values: Some(vec!["ACTIVE".to_string(), "INACTIVE".to_string()]),
1581 comment: None,
1582 },
1583 ],
1584 indexes: vec![
1585 IndexMetadata {
1586 name: "email_unique".to_string(),
1587 columns: vec!["email".to_string()],
1588 unique: true,
1589 },
1590 IndexMetadata {
1591 name: "idx_status".to_string(),
1592 columns: vec!["status".to_string()],
1593 unique: false,
1594 },
1595 ],
1596 foreign_keys: vec![],
1597 primary_key: Some(PrimaryKey {
1598 columns: vec!["id".to_string()],
1599 }),
1600 }
1601 }
1602
1603 #[test]
1604 fn test_collect_method_signatures() {
1605 let table = make_table();
1606 let sigs = collect_method_signatures(&table);
1607
1608 assert_eq!(sigs.len(), 3);
1610
1611 let id_sig = sigs.get(&vec!["id".to_string()]).unwrap();
1612 assert!(id_sig.is_unique);
1613 assert_eq!(id_sig.priority, PRIORITY_PRIMARY_KEY);
1614
1615 let email_sig = sigs.get(&vec!["email".to_string()]).unwrap();
1616 assert!(email_sig.is_unique);
1617 assert_eq!(email_sig.priority, PRIORITY_UNIQUE_INDEX);
1618
1619 let status_sig = sigs.get(&vec!["status".to_string()]).unwrap();
1620 assert!(!status_sig.is_unique);
1621 assert_eq!(status_sig.priority, PRIORITY_NON_UNIQUE_INDEX);
1622 }
1623
1624 #[test]
1625 fn test_build_select_columns() {
1626 let table = make_table();
1627 let cols = build_select_columns(&table);
1628 assert!(cols.contains("`id`"));
1629 assert!(cols.contains("`email`"));
1630 assert!(cols.contains("`status`"));
1631 }
1632
1633 #[test]
1634 fn test_build_where_clause() {
1635 let clause = build_where_clause(&["id".to_string()]);
1636 assert_eq!(clause, "`id` = ?");
1637
1638 let clause = build_where_clause(&["user_id".to_string(), "role_id".to_string()]);
1639 assert_eq!(clause, "`user_id` = ? AND `role_id` = ?");
1640 }
1641
1642 #[test]
1643 fn test_generate_upsert_method() {
1644 let table = make_table();
1645 let code = generate_upsert_method(&table, "Users");
1646
1647 assert!(code.contains("pub async fn upsert"));
1649 assert!(code.contains("ON DUPLICATE KEY UPDATE"));
1651 assert!(!code.contains("`id` = VALUES(`id`)"));
1653 assert!(code.contains("`email` = VALUES(`email`)"));
1655 assert!(code.contains("`status` = VALUES(`status`)"));
1656 }
1657
1658 #[test]
1659 fn test_generate_upsert_method_no_pk() {
1660 let mut table = make_table();
1661 table.primary_key = None;
1662 table.indexes.clear();
1663
1664 let code = generate_upsert_method(&table, "Users");
1665 assert!(code.is_empty());
1667 }
1668
1669 #[test]
1670 fn test_generate_insert_all_method() {
1671 let table = make_table();
1672 let code = generate_insert_all_method(&table, "Users");
1673
1674 assert!(code.contains("pub async fn insert_all"));
1676 assert!(code.contains("rdbi::BatchInsert::new"));
1678 }
1679
1680 fn make_column_map(table: &TableMetadata) -> HashMap<&str, &ColumnMetadata> {
1681 table.columns.iter().map(|c| (c.name.as_str(), c)).collect()
1682 }
1683
1684 #[test]
1685 fn test_generate_set_struct() {
1686 let table = make_table();
1687 let code = generate_set_struct(&table, "Users");
1688
1689 assert!(code.contains("pub struct SetUsers"));
1690 assert!(code.contains("pub email: Option<String>,"));
1691 assert!(code.contains("pub status: Option<UsersStatus>,"));
1692 assert!(!code.contains("pub id:"));
1694 }
1695
1696 #[test]
1697 fn test_generate_partial_update_method_pk() {
1698 let table = make_table();
1699 let column_map = make_column_map(&table);
1700 let sig = MethodSignature::new(
1701 vec!["id".to_string()],
1702 PRIORITY_PRIMARY_KEY,
1703 true,
1704 "PRIMARY_KEY",
1705 );
1706 let code = generate_partial_update_method(&table, &sig, &column_map, "Users");
1707
1708 assert!(code.contains(
1709 "pub async fn update_partial_by_id<P: Pool>(pool: &P, id: i64, set: SetUsers)"
1710 ));
1711 assert!(code.contains("UPDATE `users` SET {} WHERE `id` = ?"));
1712 assert!(code.contains("if sets.is_empty()"));
1713 assert!(code.contains("if let Some(v) = set.email.as_ref()"));
1714 }
1715
1716 #[test]
1717 fn test_generate_partial_update_method_nullable_where() {
1718 let mut table = make_table();
1719 table.columns[1].nullable = true; let column_map = make_column_map(&table);
1721 let sig = MethodSignature::new(
1722 vec!["email".to_string()],
1723 PRIORITY_UNIQUE_INDEX,
1724 true,
1725 "UNIQUE_INDEX",
1726 );
1727 let code = generate_partial_update_method(&table, &sig, &column_map, "Users");
1728
1729 assert!(code.contains("UPDATE `users` SET {} WHERE {}"));
1730 assert!(code.contains("`email` IS NULL"));
1731 }
1732
1733 #[test]
1734 fn test_generate_delete_and_count_by_index() {
1735 let table = make_table();
1736 let column_map = make_column_map(&table);
1737 let sig = MethodSignature::new(
1738 vec!["status".to_string()],
1739 PRIORITY_NON_UNIQUE_INDEX,
1740 false,
1741 "NON_UNIQUE_INDEX",
1742 );
1743
1744 let del = generate_delete_by_method(&table, &sig, &column_map);
1745 assert!(del.contains("pub async fn delete_by_status"));
1746 assert!(del.contains("DELETE FROM `users` WHERE `status` = ?"));
1747
1748 let cnt = generate_count_by_method(&table, &sig, &column_map);
1749 assert!(cnt.contains("pub async fn count_by_status"));
1750 assert!(cnt.contains("SELECT COUNT(*) FROM `users` WHERE `status` = ?"));
1751 }
1752
1753 #[test]
1754 fn test_generate_pagination_methods() {
1755 let table = make_table();
1756 let select_columns = build_select_columns(&table);
1757 let code = generate_pagination_methods(&table, "Users", &select_columns, "models");
1758
1759 assert!(code.contains("pub async fn find_all_paginated"));
1761 assert!(code.contains("limit: i32"));
1763 assert!(code.contains("offset: i32"));
1764 assert!(code.contains("UsersSortBy"));
1766 assert!(code.contains("SortDirection"));
1768 assert!(code.contains("pub async fn get_paginated_result"));
1770 assert!(code.contains("PaginatedResult<Users>"));
1772 }
1773}