Skip to main content

rdbi_codegen/codegen/
dao_generator.rs

1//! DAO generator - generates async rdbi query functions from table metadata
2
3use 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
20/// Priority levels for method signature deduplication
21const 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/// Represents a method signature for deduplication
27#[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
49/// Generate DAO files for all tables
50pub 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    // Generate mod.rs
55    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    // Generate each DAO file
70    for table in tables {
71        generate_dao_file(table, output_dir, &config.models_module)?;
72    }
73
74    Ok(())
75}
76
77/// Generate a single DAO file for a table
78fn 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    // Build column map for quick lookup
86    let column_map: HashMap<&str, &ColumnMetadata> =
87        table.columns.iter().map(|c| (c.name.as_str(), c)).collect();
88
89    // Collect import requirements
90    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    // Check if we have enum columns for imports
104    let has_enums = table.columns.iter().any(|c| c.is_enum());
105
106    // Generate imports
107    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        // Import enum types from the same struct module
112        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    // Generate select columns list
132    let select_columns = build_select_columns(table);
133
134    // Generate find_all
135    code.push_str(&generate_find_all(table, &struct_name, &select_columns));
136
137    // Generate count_all
138    code.push_str(&generate_count_all(table));
139
140    // Generate primary key methods
141    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    // Generate insert methods
152    code.push_str(&generate_insert_methods(table, &struct_name, &column_map));
153
154    // Generate insert_plain method (individual params)
155    code.push_str(&generate_insert_plain_method(table, &column_map));
156
157    // Generate batch insert method
158    code.push_str(&generate_insert_all_method(table, &struct_name));
159
160    // Generate upsert method
161    code.push_str(&generate_upsert_method(table, &struct_name));
162
163    // Generate update methods
164    if table.primary_key.is_some() {
165        code.push_str(&generate_update_methods(table, &struct_name, &column_map));
166        // Generate update_plain method (individual params)
167        code.push_str(&generate_update_plain_method(table, &column_map));
168    }
169
170    // Generate index-aware findBy methods (sorted so output is deterministic)
171    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        // Skip if this is the primary key (already generated separately)
177        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    // Generate partial-update methods (Set struct + update_partial_by_*)
190    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    // Generate delete_by / count_by for secondary index signatures
203    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    // Generate list-based findBy methods for single-column indexes
212    code.push_str(&generate_find_by_list_methods(
213        table,
214        &column_map,
215        &struct_name,
216        &select_columns,
217    ));
218
219    // Generate composite enum list methods (e.g., find_by_user_id_and_device_types)
220    code.push_str(&generate_composite_enum_list_methods(
221        table,
222        &column_map,
223        &struct_name,
224        &select_columns,
225    ));
226
227    // Generate pagination methods
228    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
240/// Build the SELECT columns list
241fn 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
250/// Build the WHERE clause for columns
251fn build_where_clause(columns: &[String]) -> String {
252    columns
253        .iter()
254        .map(|c| format!("`{}` = ?", c))
255        .collect::<Vec<_>>()
256        .join(" AND ")
257}
258
259/// Build parameter list for a function signature
260fn 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
277/// Generate bind calls for query
278fn 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
286/// Generate find_all function
287fn 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
301/// Generate count_all function
302fn 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
314/// Generate primary key methods (find, delete)
315fn 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    // find_by_pk
330    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    // delete_by_pk
348    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
367/// Generate insert methods
368fn 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    // Get non-auto-increment columns for insert
376    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    // insert (with entity)
413    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
431/// Generate insert_plain method with individual parameters
432fn generate_insert_plain_method(
433    table: &TableMetadata,
434    column_map: &HashMap<&str, &ColumnMetadata>,
435) -> String {
436    // Get non-auto-increment columns for insert
437    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
481/// Generate batch insert method (insert_all) using BatchInsert
482fn generate_insert_all_method(table: &TableMetadata, struct_name: &str) -> String {
483    // Get non-auto-increment columns for insert
484    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
506/// Check if table has a unique index (excluding primary key)
507fn has_unique_index(table: &TableMetadata) -> bool {
508    table.indexes.iter().any(|idx| idx.unique)
509}
510
511/// Generate upsert method (INSERT ... ON DUPLICATE KEY UPDATE)
512fn generate_upsert_method(table: &TableMetadata, struct_name: &str) -> String {
513    // Only generate if table has primary key or unique index
514    if table.primary_key.is_none() && !has_unique_index(table) {
515        return String::new();
516    }
517
518    // Get non-auto-increment columns for insert
519    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    // Get primary key columns for exclusion from UPDATE clause
530    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    // Columns to update on duplicate key (all non-PK, non-auto-increment columns)
537    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 no columns to update, skip upsert generation
544    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
598/// Generate update methods
599fn 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    // Get non-PK columns for SET clause
609    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    // Bind update columns first, then PK columns
628    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
669/// Generate update_plain method with individual parameters (update_by_<pk>)
670fn 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    // Get non-PK columns for SET clause
677    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    // Build method name based on PK columns
688    let method_name = generate_update_by_method_name(&pk.columns);
689
690    // Build params: PK columns first, then update columns
691    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    // Bind update columns first (for SET), then PK columns (for WHERE)
705    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
728/// Generate the Set<Struct> partial-update struct for a table: one Option field
729/// per non-primary-key column.
730fn 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
770/// Build generated statements that push NULL-aware WHERE conditions into a
771/// `wheres: Vec<String>` local, plus the matching DynamicQuery bind statements.
772fn 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
802/// Generate an update_partial_by_<cols> method for one method signature.
803fn 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
912/// Generate a delete_by method for a secondary index/FK signature.
913fn 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
968/// Generate a count_by method for a secondary index/FK signature.
969fn 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
1024/// Collect method signatures with priority-based deduplication
1025fn collect_method_signatures(table: &TableMetadata) -> HashMap<Vec<String>, MethodSignature> {
1026    let mut signatures: HashMap<Vec<String>, MethodSignature> = HashMap::new();
1027
1028    // Priority 1: Primary key
1029    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    // Priority 2 & 3: Indexes
1040    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        // Only add if no higher priority signature exists
1054        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    // Priority 4: Foreign keys
1064    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
1074/// Generate a find_by method for an index/FK
1075fn 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    // Check if any column is nullable - if so, we need dynamic SQL for IS NULL handling
1097    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 dynamic query that handles NULL values properly
1106        generate_find_by_method_nullable(
1107            table,
1108            sig,
1109            column_map,
1110            select_columns,
1111            &params,
1112            &return_type,
1113            fetch_method,
1114            return_desc,
1115        )
1116    } else {
1117        // Use static query for non-nullable columns
1118        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/// Generate a find_by method that handles nullable columns with IS NULL
1144#[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    // Build the where clause conditions and bind logic
1156    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            // For nullable columns, check if value is None and use IS NULL
1165            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        // Join with " AND "
1184        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
1217/// Generate list-based findBy methods for single-column indexes
1218fn 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    // Primary key (if single column)
1228    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    // Single-column indexes
1243    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
1262/// Generate a single find_by_<column>s method (using IN clause)
1263fn 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    // Get the inner type (unwrap Option if nullable)
1276    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
1303/// Generate composite enum list methods for multi-column indexes with enum columns
1304/// Example: find_by_user_id_and_device_types(user_id, &[DeviceType])
1305fn 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        // Skip single-column indexes (handled by generate_find_by_list_methods)
1315        if index.columns.len() <= 1 {
1316            continue;
1317        }
1318
1319        // Identify which columns are enums
1320        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        // Skip if no enum columns
1333        if enum_columns.is_empty() {
1334            continue;
1335        }
1336
1337        // Skip if first column is enum (for optimal index usage, equality should be on leading column)
1338        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
1356/// Generate a single composite enum list method
1357fn 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    // Build method name: pluralize enum column names
1366    let method_name = generate_composite_enum_method_name(columns, enum_columns);
1367
1368    // Build params and WHERE clause parts
1369    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            // Enum column uses list parameter
1378            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            // Non-enum column uses single value
1383            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    // Build WHERE clause with proper placeholders
1392    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) // placeholder for IN clause
1397            } else {
1398                format!("`{}` = ?", col_name)
1399            }
1400        })
1401        .collect();
1402
1403    // Build the bind section
1404    let mut bind_code = String::new();
1405
1406    // First bind non-enum (single value) columns
1407    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    // Then bind enum (list) columns
1415    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    // Build the column name description for doc comment
1423    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    // Build dynamic WHERE clause construction
1435    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    // Generate the IN clause placeholders dynamically
1442    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    // Build format args for the WHERE clause
1454    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
1479/// Generate empty checks for enum list parameters
1480fn 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
1494/// Generate method name for composite enum queries
1495/// Example: ["user_id", "device_type"] with enum_columns={"device_type"} -> "find_by_user_id_and_device_types"
1496fn 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
1508/// Generate pagination methods (find_all_paginated, get_paginated_result)
1509fn 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        // Should have signatures for: id (PK), email (unique), status (non-unique)
1609        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        // Should contain upsert function
1648        assert!(code.contains("pub async fn upsert"));
1649        // Should contain ON DUPLICATE KEY UPDATE
1650        assert!(code.contains("ON DUPLICATE KEY UPDATE"));
1651        // Should NOT update the PK column (id)
1652        assert!(!code.contains("`id` = VALUES(`id`)"));
1653        // Should update non-PK columns
1654        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        // Should not generate upsert without PK or unique index
1666        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        // Should contain insert_all function
1675        assert!(code.contains("pub async fn insert_all"));
1676        // Should use BatchInsert
1677        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        // Primary key columns are not settable
1693        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; // email
1720        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        // Should contain find_all_paginated function
1760        assert!(code.contains("pub async fn find_all_paginated"));
1761        // Should have limit and offset params
1762        assert!(code.contains("limit: i32"));
1763        assert!(code.contains("offset: i32"));
1764        // Should use SortBy enum
1765        assert!(code.contains("UsersSortBy"));
1766        // Should use SortDirection
1767        assert!(code.contains("SortDirection"));
1768        // Should contain get_paginated_result
1769        assert!(code.contains("pub async fn get_paginated_result"));
1770        // Should use PaginatedResult
1771        assert!(code.contains("PaginatedResult<Users>"));
1772    }
1773}