Skip to main content

drizzle_migrations/postgres/
codegen.rs

1//! `PostgreSQL` schema code generation
2//!
3//! This module generates Rust source code from introspected DDL entities.
4//! The generated code uses the lowercase attribute syntax (e.g., `primary` instead of `PRIMARY`)
5//! that is the current recommended style.
6
7use super::collection::PostgresDDL;
8use super::ddl::{
9    CheckConstraint, Column, Enum, ForeignKey, Index, Policy, Table, UniqueConstraint, View,
10};
11use crate::utils::escape_for_rust_literal;
12use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase};
13use std::collections::{HashMap, HashSet};
14use std::fmt::Write;
15
16/// Result of code generation
17#[derive(Debug, Clone, Default)]
18pub struct GeneratedSchema {
19    /// The generated Rust source code
20    pub code: String,
21    /// Enums that were generated
22    pub enums: Vec<String>,
23    /// Tables that were generated
24    pub tables: Vec<String>,
25    /// Indexes that were generated
26    pub indexes: Vec<String>,
27    /// Views that were generated
28    pub views: Vec<String>,
29    /// Policies that were generated
30    pub policies: Vec<String>,
31    /// Any warnings during generation
32    pub warnings: Vec<String>,
33}
34
35/// Options for code generation
36#[derive(Debug, Clone, Default)]
37pub struct CodegenOptions {
38    /// Module documentation
39    pub module_doc: Option<String>,
40    /// Whether to include a schema struct
41    pub include_schema: bool,
42    /// Schema struct name
43    pub schema_name: String,
44    /// Whether to use public visibility
45    pub use_pub: bool,
46    /// Field naming style for generated Rust members
47    pub field_casing: FieldCasing,
48}
49
50/// Casing strategy for generated Rust field names.
51#[derive(Debug, Clone, Copy, Default)]
52pub enum FieldCasing {
53    /// `snake_case` (default)
54    #[default]
55    Snake,
56    /// `camelCase`
57    Camel,
58    /// Preserve source casing as much as possible
59    Preserve,
60}
61
62fn sanitize_rust_identifier(name: &str) -> String {
63    let mut out = String::with_capacity(name.len());
64    for (idx, ch) in name.chars().enumerate() {
65        let valid = if idx == 0 {
66            ch == '_' || ch.is_ascii_alphabetic()
67        } else {
68            ch == '_' || ch.is_ascii_alphanumeric()
69        };
70
71        if valid {
72            out.push(ch);
73        } else {
74            out.push('_');
75        }
76    }
77
78    if out.is_empty() { "_".to_string() } else { out }
79}
80
81fn apply_field_casing(name: &str, casing: FieldCasing) -> String {
82    match casing {
83        FieldCasing::Snake => name.to_snake_case(),
84        FieldCasing::Camel => name.to_lower_camel_case(),
85        FieldCasing::Preserve => sanitize_rust_identifier(name),
86    }
87}
88
89/// Lookup tables derived from a [`PostgresDDL`] and shared across every
90/// per-entity generation pass.
91struct SchemaMaps<'a> {
92    enum_map: HashMap<(String, String), String>,
93    table_columns: HashMap<(String, String), Vec<&'a Column>>,
94    table_pks: HashMap<(String, String), HashSet<String>>,
95    single_unique_columns: HashMap<(String, String), HashSet<String>>,
96    table_uniques: HashMap<(String, String), Vec<&'a UniqueConstraint>>,
97    table_checks: HashMap<(String, String), Vec<&'a CheckConstraint>>,
98    fk_map: HashMap<(String, String, String), (&'a ForeignKey, usize)>,
99}
100
101fn build_schema_maps(ddl: &PostgresDDL) -> SchemaMaps<'_> {
102    let mut enum_map: HashMap<(String, String), String> = HashMap::new();
103    for e in ddl.enums.list() {
104        let type_name = e.name.to_pascal_case();
105        enum_map.insert((e.schema.to_string(), e.name.to_string()), type_name);
106    }
107
108    let mut table_columns: HashMap<(String, String), Vec<&Column>> = HashMap::new();
109    for column in ddl.columns.list() {
110        table_columns
111            .entry((column.schema.to_string(), column.table.to_string()))
112            .or_default()
113            .push(column);
114    }
115
116    let mut table_pks: HashMap<(String, String), HashSet<String>> = HashMap::new();
117    for pk in ddl.pks.list() {
118        for col in pk.columns.iter() {
119            table_pks
120                .entry((pk.schema.to_string(), pk.table.to_string()))
121                .or_default()
122                .insert(col.to_string());
123        }
124    }
125
126    let mut single_unique_columns: HashMap<(String, String), HashSet<String>> = HashMap::new();
127    let mut table_uniques: HashMap<(String, String), Vec<&UniqueConstraint>> = HashMap::new();
128    for unique in ddl.uniques.list() {
129        let key = (unique.schema.to_string(), unique.table.to_string());
130        table_uniques.entry(key.clone()).or_default().push(unique);
131        if unique.columns.len() == 1
132            && !unique.name_explicit
133            && !unique.deferrable
134            && !unique.initially_deferred
135            && !unique.nulls_not_distinct
136        {
137            single_unique_columns
138                .entry((unique.schema.to_string(), unique.table.to_string()))
139                .or_default()
140                .insert(unique.columns[0].to_string());
141        }
142    }
143
144    let mut table_checks: HashMap<(String, String), Vec<&CheckConstraint>> = HashMap::new();
145    for check in ddl.checks.list() {
146        table_checks
147            .entry((check.schema.to_string(), check.table.to_string()))
148            .or_default()
149            .push(check);
150    }
151
152    let mut fk_map: HashMap<(String, String, String), (&ForeignKey, usize)> = HashMap::new();
153    for fk in ddl.fks.list() {
154        for (idx, col) in fk.columns.iter().enumerate() {
155            fk_map.insert(
156                (fk.schema.to_string(), fk.table.to_string(), col.to_string()),
157                (fk, idx),
158            );
159        }
160    }
161
162    SchemaMaps {
163        enum_map,
164        table_columns,
165        table_pks,
166        single_unique_columns,
167        table_uniques,
168        table_checks,
169        fk_map,
170    }
171}
172
173fn write_module_header(code: &mut String, options: &CodegenOptions) {
174    code.push_str("//! Auto-generated PostgreSQL schema from introspection\n");
175    code.push_str("//!\n");
176    if let Some(doc) = &options.module_doc {
177        for line in doc.lines() {
178            code.push_str("//! ");
179            code.push_str(line);
180            code.push('\n');
181        }
182    }
183    code.push('\n');
184    code.push_str("use drizzle::postgres::prelude::*;\n\n");
185}
186
187/// Generate Rust schema code from DDL
188#[must_use]
189pub fn generate_rust_schema(ddl: &PostgresDDL, options: &CodegenOptions) -> GeneratedSchema {
190    let mut result = GeneratedSchema::default();
191    let mut code = String::new();
192
193    write_module_header(&mut code, options);
194
195    let maps = build_schema_maps(ddl);
196
197    // Generate enum definitions
198    for e in ddl.enums.list() {
199        code.push_str(&generate_enum_struct(e, options.use_pub));
200        code.push('\n');
201        result.enums.push(e.name.to_string());
202    }
203
204    // Generate table structs
205    for table in ddl.tables.list() {
206        let key = (table.schema.to_string(), table.name.to_string());
207        let columns = maps
208            .table_columns
209            .get(&key)
210            .map_or(&[][..], std::vec::Vec::as_slice);
211        let pk_columns = maps.table_pks.get(&key);
212        let unique_columns = maps.single_unique_columns.get(&key);
213        let unique_constraints = maps
214            .table_uniques
215            .get(&key)
216            .map_or(&[][..], std::vec::Vec::as_slice);
217        let check_constraints = maps
218            .table_checks
219            .get(&key)
220            .map_or(&[][..], std::vec::Vec::as_slice);
221        let is_composite_pk = pk_columns.is_some_and(|pks| pks.len() > 1);
222
223        code.push_str(&generate_table_struct(&TableGenContext {
224            table,
225            columns,
226            pk_columns,
227            unique_columns,
228            unique_constraints,
229            check_constraints,
230            is_composite_pk,
231            fk_map: &maps.fk_map,
232            enum_map: &maps.enum_map,
233            use_pub: options.use_pub,
234            field_casing: options.field_casing,
235        }));
236        code.push('\n');
237        result.tables.push(table.name.to_string());
238    }
239
240    // Generate index structs
241    for index in ddl.indexes.list() {
242        code.push_str(&generate_index_struct(
243            index,
244            options.use_pub,
245            options.field_casing,
246        ));
247        code.push('\n');
248        result.indexes.push(index.name.to_string());
249    }
250
251    // Generate view structs
252    for view in ddl.views.list() {
253        if view.is_existing {
254            continue;
255        }
256        let key = (view.schema.to_string(), view.name.to_string());
257        let columns = maps
258            .table_columns
259            .get(&key)
260            .map_or(&[][..], std::vec::Vec::as_slice);
261        code.push_str(&generate_view_struct(
262            view,
263            columns,
264            &maps.enum_map,
265            options.use_pub,
266            options.field_casing,
267        ));
268        code.push('\n');
269        result.views.push(view.name.to_string());
270    }
271
272    for policy in ddl.policies.list() {
273        code.push_str(&generate_policy_struct(policy, options.use_pub));
274        code.push('\n');
275        result.policies.push(policy.name.to_string());
276    }
277
278    if options.include_schema {
279        code.push_str(&generate_schema_struct(
280            &options.schema_name,
281            &result.tables,
282            &result.indexes,
283            &result.policies,
284            options.use_pub,
285            options.field_casing,
286        ));
287    }
288
289    result.code = code;
290    result
291}
292
293/// Context for generating a table struct
294struct TableGenContext<'a> {
295    table: &'a Table,
296    columns: &'a [&'a Column],
297    pk_columns: Option<&'a HashSet<String>>,
298    unique_columns: Option<&'a HashSet<String>>,
299    unique_constraints: &'a [&'a UniqueConstraint],
300    check_constraints: &'a [&'a CheckConstraint],
301    is_composite_pk: bool,
302    fk_map: &'a HashMap<(String, String, String), (&'a ForeignKey, usize)>,
303    enum_map: &'a HashMap<(String, String), String>,
304    use_pub: bool,
305    field_casing: FieldCasing,
306}
307
308/// Generate a single table struct
309fn generate_table_struct(ctx: &TableGenContext<'_>) -> String {
310    let struct_name = ctx.table.name.to_pascal_case();
311    let vis = if ctx.use_pub { "pub " } else { "" };
312
313    let mut code = String::new();
314
315    if let Some(comment) = ctx.table.comment.as_deref() {
316        write_doc_comment(&mut code, "", comment);
317    }
318
319    // Table attribute
320    let table_attrs = format_table_attrs(ctx);
321    if table_attrs.is_empty() {
322        code.push_str("#[PostgresTable]\n");
323    } else {
324        let _ = writeln!(code, "#[PostgresTable({})]", table_attrs.join(", "));
325    }
326
327    // Struct definition
328    let _ = writeln!(code, "{vis}struct {struct_name} {{");
329
330    // Sort columns by ordinal position if available, falling back to name.
331    let mut sorted_columns: Vec<&&Column> = ctx.columns.iter().collect();
332    sorted_columns.sort_by(|a, b| {
333        let ao = a.ordinal_position.unwrap_or(i32::MAX);
334        let bo = b.ordinal_position.unwrap_or(i32::MAX);
335        ao.cmp(&bo).then_with(|| a.name.cmp(&b.name))
336    });
337
338    // Generate fields
339    for column in sorted_columns {
340        let field_code = generate_column_field(column, ctx);
341        code.push_str(&field_code);
342    }
343
344    code.push_str("}\n");
345    code
346}
347
348fn format_table_attrs(ctx: &TableGenContext<'_>) -> Vec<String> {
349    let table = ctx.table;
350    let mut attrs = Vec::new();
351    if table.schema != "public" {
352        attrs.push(format!(
353            "schema = \"{}\"",
354            escape_for_rust_literal(&table.schema)
355        ));
356    }
357    if table.is_unlogged == Some(true) {
358        attrs.push("unlogged".to_string());
359    }
360    if table.is_temporary == Some(true) {
361        attrs.push("temporary".to_string());
362    }
363    if let Some(inherits) = &table.inherits {
364        attrs.push(format!(
365            "inherits = \"{}\"",
366            escape_for_rust_literal(inherits)
367        ));
368    }
369    if let Some(tablespace) = &table.tablespace {
370        attrs.push(format!(
371            "tablespace = \"{}\"",
372            escape_for_rust_literal(tablespace)
373        ));
374    }
375    if table.is_rls_enabled == Some(true) {
376        attrs.push("rls".to_string());
377    }
378    for unique in ctx.unique_constraints {
379        if should_emit_table_unique(unique) {
380            attrs.push(format_table_unique_attr(unique, ctx.field_casing));
381        }
382    }
383    for (idx, check) in ctx.check_constraints.iter().enumerate() {
384        if check_column_target(check, ctx).is_none() {
385            attrs.push(format_table_check_attr(check, ctx, idx));
386        }
387    }
388    attrs
389}
390
391fn should_emit_table_unique(unique: &UniqueConstraint) -> bool {
392    unique.columns.len() > 1
393        || unique.name_explicit
394        || unique.deferrable
395        || unique.initially_deferred
396        || unique.nulls_not_distinct
397}
398
399fn default_unique_name(table: &str, columns: &[impl AsRef<str>]) -> String {
400    format!(
401        "{}_{}_key",
402        table,
403        columns
404            .iter()
405            .map(AsRef::as_ref)
406            .collect::<Vec<_>>()
407            .join("_")
408    )
409}
410
411fn format_table_unique_attr(unique: &UniqueConstraint, field_casing: FieldCasing) -> String {
412    let columns: Vec<String> = unique
413        .columns
414        .iter()
415        .map(|col| apply_field_casing(col.as_ref(), field_casing))
416        .collect();
417    let mut args = vec![format!("columns({})", columns.join(", "))];
418    let default_name = default_unique_name(&unique.table, &unique.columns);
419    if unique.name_explicit || unique.name != default_name {
420        args.push(format!(
421            "name = \"{}\"",
422            escape_for_rust_literal(&unique.name)
423        ));
424    }
425    if unique.nulls_not_distinct {
426        args.push("nulls_not_distinct".to_string());
427    }
428    if unique.deferrable {
429        args.push("deferrable".to_string());
430    }
431    if unique.initially_deferred {
432        args.push("initially_deferred".to_string());
433    }
434    format!("unique({})", args.join(", "))
435}
436
437fn format_table_check_attr(
438    check: &CheckConstraint,
439    _ctx: &TableGenContext<'_>,
440    _idx: usize,
441) -> String {
442    let mut args = Vec::new();
443    args.push(format!(
444        "name = \"{}\"",
445        escape_for_rust_literal(&check.name)
446    ));
447    args.push(format!(
448        "expr = \"{}\"",
449        escape_for_rust_literal(&check.value)
450    ));
451    format!("check({})", args.join(", "))
452}
453
454fn check_column_target(check: &CheckConstraint, ctx: &TableGenContext<'_>) -> Option<String> {
455    let referenced = expression_referenced_columns(&check.value, ctx.columns);
456    if referenced.len() != 1 {
457        return None;
458    }
459    let column = referenced.into_iter().next()?;
460    if check.name == format!("{}_{}_check", ctx.table.name, column) {
461        Some(column)
462    } else {
463        None
464    }
465}
466
467fn expression_referenced_columns(expr: &str, columns: &[&Column]) -> Vec<String> {
468    columns
469        .iter()
470        .filter_map(|column| {
471            let name = column.name.as_ref();
472            if expression_references_identifier(expr, name) {
473                Some(name.to_string())
474            } else {
475                None
476            }
477        })
478        .collect()
479}
480
481fn expression_references_identifier(expr: &str, ident: &str) -> bool {
482    let expr_lower = expr.to_ascii_lowercase();
483    let ident_lower = ident.to_ascii_lowercase();
484    if expr_lower.contains(&format!("\"{ident_lower}\"")) {
485        return true;
486    }
487
488    let mut offset = 0;
489    while let Some(pos) = expr_lower[offset..].find(&ident_lower) {
490        let start = offset + pos;
491        let end = start + ident_lower.len();
492        let before = expr_lower[..start].chars().next_back();
493        let after = expr_lower[end..].chars().next();
494        let before_boundary = before.is_none_or(|c| !(c == '_' || c.is_ascii_alphanumeric()));
495        let after_boundary = after.is_none_or(|c| !(c == '_' || c.is_ascii_alphanumeric()));
496        if before_boundary && after_boundary {
497            return true;
498        }
499        offset = end;
500    }
501    false
502}
503
504fn column_check_for<'a>(column: &Column, ctx: &TableGenContext<'a>) -> Option<&'a CheckConstraint> {
505    ctx.check_constraints
506        .iter()
507        .copied()
508        .find(|check| check_column_target(check, ctx).as_deref() == Some(column.name.as_ref()))
509}
510
511/// Format a column's IDENTITY metadata as a `#[column(identity(...))]` fragment.
512///
513/// `sql_type` is used to suppress options equal to `PostgreSQL`'s defaults
514/// for the column's type — introspection materializes every option, and
515/// re-emitting the defaults would clutter every identity column with
516/// `min_value = 1, max_value = 2147483647`.
517fn format_identity_attr(identity: &super::ddl::Identity, sql_type: &str) -> String {
518    use super::ddl::IdentityType;
519    use super::grammar::{IdentityDefaults, PgTypeCategory};
520
521    let identity_type = match identity.type_ {
522        IdentityType::Always => "always",
523        IdentityType::ByDefault => "by_default",
524    };
525
526    let default_range_type = match PgTypeCategory::from_sql_type(sql_type) {
527        PgTypeCategory::SmallInt => "smallint",
528        PgTypeCategory::BigInt => "bigint",
529        _ => "integer",
530    };
531
532    let mut seq_opts: Vec<String> = Vec::new();
533    if let Some(increment) = &identity.increment
534        && increment != IdentityDefaults::INCREMENT
535    {
536        seq_opts.push(format!("increment = {increment}"));
537    }
538    if let Some(start) = &identity.start_with
539        && start != IdentityDefaults::START_WITH
540    {
541        seq_opts.push(format!("start = {start}"));
542    }
543    if let Some(min) = &identity.min_value
544        && min != IdentityDefaults::MIN
545    {
546        seq_opts.push(format!("min_value = {min}"));
547    }
548    if let Some(max) = &identity.max_value
549        && max != IdentityDefaults::max_for(default_range_type)
550    {
551        seq_opts.push(format!("max_value = {max}"));
552    }
553    if let Some(cache) = &identity.cache
554        && *cache != IdentityDefaults::CACHE
555    {
556        seq_opts.push(format!("cache = {cache}"));
557    }
558    if identity.cycle == Some(true) {
559        seq_opts.push("cycle".to_string());
560    }
561
562    if seq_opts.is_empty() {
563        format!("identity({identity_type})")
564    } else {
565        format!("identity({identity_type}, {})", seq_opts.join(", "))
566    }
567}
568
569/// Push FK-related attributes (`references`, `on_delete`, `on_update`) for a
570/// column onto the accumulator.
571fn push_fk_attrs(attrs: &mut Vec<String>, fk: &ForeignKey, idx: usize) {
572    let ref_table = fk.table_to.to_pascal_case();
573    let ref_column = fk.columns_to.get(idx).cloned().unwrap_or_default();
574    attrs.push(format!("references = {ref_table}::{ref_column}"));
575
576    if let Some(on_delete) = &fk.on_delete
577        && on_delete != "NO ACTION"
578    {
579        let action = on_delete.to_lowercase().replace(' ', "_");
580        attrs.push(format!("on_delete = {action}"));
581    }
582
583    if let Some(on_update) = &fk.on_update
584        && on_update != "NO ACTION"
585    {
586        let action = on_update.to_lowercase().replace(' ', "_");
587        attrs.push(format!("on_update = {action}"));
588    }
589
590    if fk.deferrable {
591        attrs.push("deferrable".to_string());
592    }
593    if fk.initially_deferred {
594        attrs.push("initially_deferred".to_string());
595    }
596}
597
598/// Generate a single column as a struct field
599fn generate_column_field(column: &Column, ctx: &TableGenContext<'_>) -> String {
600    let field_name = apply_field_casing(column.name.as_ref(), ctx.field_casing);
601    let vis = if ctx.use_pub { "pub " } else { "" };
602
603    let col_name_str = column.name.to_string();
604    let is_pk = ctx
605        .pk_columns
606        .is_some_and(|pks| pks.contains(&col_name_str));
607    let is_unique = ctx
608        .unique_columns
609        .is_some_and(|uqs| uqs.contains(&col_name_str));
610
611    // For single-column PKs, add primary. For composite, skip (handled at table level)
612    let should_add_primary = is_pk && !ctx.is_composite_pk;
613
614    // Check for serial (nextval default without identity)
615    let is_serial = column
616        .default
617        .as_ref()
618        .is_some_and(|d| d.contains("nextval"))
619        && column.identity.is_none();
620
621    // Get FK info if present
622    let fk_info = ctx.fk_map.get(&(
623        column.schema.to_string(),
624        column.table.to_string(),
625        col_name_str,
626    ));
627
628    // Check if this column uses an enum type
629    let type_schema = column.type_schema.as_deref().unwrap_or(&column.schema);
630    let enum_type = ctx
631        .enum_map
632        .get(&(type_schema.to_string(), column.sql_type.to_string()));
633
634    // Build column attributes
635    let mut attrs = Vec::new();
636
637    // For SERIAL columns (auto-increment via nextval), use "serial" attribute
638    if is_serial {
639        attrs.push("serial".to_string());
640    }
641
642    // For GENERATED IDENTITY columns, use identity(always) or identity(by_default)
643    // with optional sequence options
644    if let Some(identity) = &column.identity {
645        attrs.push(format_identity_attr(identity, &column.sql_type));
646    }
647
648    if should_add_primary {
649        attrs.push("primary".to_string());
650    }
651
652    if is_unique {
653        attrs.push("unique".to_string());
654    }
655
656    // Add "enum" attribute for enum-typed columns
657    if enum_type.is_some() {
658        attrs.push("enum".to_string());
659    }
660
661    if let Some(collate) = &column.collate {
662        attrs.push(format!(
663            "collate = \"{}\"",
664            escape_for_rust_literal(collate)
665        ));
666    }
667
668    // Add generated column attribute for GENERATED AS columns
669    if let Some(generated) = &column.generated {
670        use super::ddl::GeneratedType;
671        let gen_type = match generated.gen_type {
672            GeneratedType::Stored => "stored",
673            GeneratedType::Virtual => "virtual",
674        };
675        let expr = escape_for_rust_literal(&generated.expression);
676        attrs.push(format!("generated({gen_type}, \"{expr}\")"));
677    }
678
679    // Add default if present (but skip nextval for serial columns)
680    if let Some(default) = &column.default
681        && !is_serial
682        && column.generated.is_none()
683    {
684        if let Some(formatted) = format_default_value(default, &column.sql_type) {
685            attrs.push(format!("default = {formatted}"));
686        } else if !default.trim().eq_ignore_ascii_case("null") {
687            attrs.push(format!(
688                "default_sql = \"{}\"",
689                escape_for_rust_literal(default)
690            ));
691        }
692    }
693
694    if let Some(check) = column_check_for(column, ctx) {
695        attrs.push(format!(
696            "check = \"{}\"",
697            escape_for_rust_literal(&check.value)
698        ));
699    }
700
701    // Add FK reference if present
702    if let Some((fk, idx)) = fk_info {
703        push_fk_attrs(&mut attrs, fk, *idx);
704    }
705
706    // Generate attribute line if there are any
707    let mut result = String::new();
708    if let Some(comment) = column.comment.as_deref() {
709        write_doc_comment(&mut result, "    ", comment);
710    }
711    if !attrs.is_empty() {
712        let _ = writeln!(result, "    #[column({})]", attrs.join(", "));
713    }
714
715    // Determine Rust type - use enum type if available, otherwise map SQL type
716    let rust_type = enum_type.map_or_else(
717        || {
718            sql_type_to_rust_type_with_dimensions(
719                &column.sql_type,
720                column.dimensions,
721                column.not_null,
722            )
723        },
724        |enum_name| {
725            if column.not_null {
726                enum_name.clone()
727            } else {
728                format!("Option<{enum_name}>")
729            }
730        },
731    );
732
733    let _ = writeln!(result, "    {vis}{field_name}: {rust_type},");
734    result
735}
736
737/// Generate a Rust enum definition from a `PostgreSQL` enum
738fn generate_enum_struct(e: &Enum, use_pub: bool) -> String {
739    let enum_name = e.name.to_pascal_case();
740    let vis = if use_pub { "pub " } else { "" };
741
742    let mut code = String::new();
743
744    // Enum derive attribute with PostgresEnum - matches the project's actual usage
745    // #[derive(PostgresEnum, Default, Clone, PartialEq, Debug)]
746    code.push_str("#[derive(PostgresEnum, Default, Clone, PartialEq, Debug)]\n");
747
748    // Enum definition
749    let _ = writeln!(code, "{vis}enum {enum_name} {{");
750
751    // Generate variants from enum values
752    for (idx, value) in e.values.iter().enumerate() {
753        let variant_name = value.to_pascal_case();
754        // First variant gets #[default] attribute
755        if idx == 0 {
756            code.push_str("    #[default]\n");
757        }
758        let _ = writeln!(code, "    {variant_name},");
759    }
760
761    code.push_str("}\n");
762    code
763}
764
765/// Format a default value for Rust syntax
766fn format_default_value(default: &str, sql_type: &str) -> Option<String> {
767    let default = default.trim();
768    let sql_type_lower = sql_type.to_ascii_lowercase();
769
770    // Skip defaults that are function calls (like now(), nextval(), etc.)
771    if default.contains('(') || default.starts_with("nextval") {
772        return None;
773    }
774
775    // Handle NULL
776    if default.eq_ignore_ascii_case("null") {
777        return None;
778    }
779
780    // Handle boolean
781    if default.eq_ignore_ascii_case("true") || default.eq_ignore_ascii_case("false") {
782        return Some(default.to_lowercase());
783    }
784
785    // Handle numeric types
786    if sql_type_lower.contains("int")
787        || sql_type_lower.contains("numeric")
788        || sql_type_lower.contains("decimal")
789        || sql_type_lower == "float4"
790        || sql_type_lower == "float8"
791    {
792        // Remove type casts like ::integer
793        let value = default.split("::").next().unwrap_or(default);
794        return Some(value.trim_matches('\'').to_string());
795    }
796
797    // Handle text/string types
798    if sql_type_lower.contains("text")
799        || sql_type_lower.contains("varchar")
800        || sql_type_lower.contains("char")
801        || sql_type_lower == "bpchar"
802    {
803        // Keep as quoted string, removing Postgres specific casts
804        let value = default.split("::").next().unwrap_or(default);
805        let trimmed = value.trim_matches('\'');
806        return Some(format!("\"{}\"", escape_for_rust_literal(trimmed)));
807    }
808
809    None
810}
811
812/// Convert `PostgreSQL` type to Rust type
813#[must_use]
814pub fn sql_type_to_rust_type(sql_type: &str, not_null: bool) -> String {
815    // Handle PostgreSQL array types, which are often represented as "_typename" (udt_name).
816    // Keep this intentionally simple: one-dimensional arrays map to Vec<T>.
817    if let Some(elem) = sql_type.strip_prefix('_') {
818        let elem_ty = sql_type_to_rust_type(elem, true);
819        let base = format!("Vec<{elem_ty}>");
820        return if not_null {
821            base
822        } else {
823            format!("Option<{base}>")
824        };
825    }
826
827    let base_type = match sql_type {
828        // Integer types
829        s if s.eq_ignore_ascii_case("int2") || s.eq_ignore_ascii_case("smallint") => "i16",
830        s if s.eq_ignore_ascii_case("int4")
831            || s.eq_ignore_ascii_case("integer")
832            || s.eq_ignore_ascii_case("int") =>
833        {
834            "i32"
835        }
836        s if s.eq_ignore_ascii_case("int8") || s.eq_ignore_ascii_case("bigint") => "i64",
837        s if s.eq_ignore_ascii_case("serial") || s.eq_ignore_ascii_case("serial4") => "i32",
838        s if s.eq_ignore_ascii_case("bigserial") || s.eq_ignore_ascii_case("serial8") => "i64",
839        s if s.eq_ignore_ascii_case("smallserial") || s.eq_ignore_ascii_case("serial2") => "i16",
840
841        // Floating point
842        s if s.eq_ignore_ascii_case("float4") || s.eq_ignore_ascii_case("real") => "f32",
843        s if s.eq_ignore_ascii_case("float8") || s.eq_ignore_ascii_case("double precision") => {
844            "f64"
845        }
846        s if s.eq_ignore_ascii_case("numeric") || s.eq_ignore_ascii_case("decimal") => "String", // Use String for precise decimals
847
848        // Boolean
849        s if s.eq_ignore_ascii_case("bool") || s.eq_ignore_ascii_case("boolean") => "bool",
850
851        // Text types
852        s if s.eq_ignore_ascii_case("text")
853            || s.eq_ignore_ascii_case("varchar")
854            || s.eq_ignore_ascii_case("char")
855            || s.eq_ignore_ascii_case("bpchar")
856            || s.eq_ignore_ascii_case("name") =>
857        {
858            "String"
859        }
860
861        // Binary
862        s if s.eq_ignore_ascii_case("bytea") => "Vec<u8>",
863
864        // UUID
865        s if s.eq_ignore_ascii_case("uuid") => "uuid::Uuid",
866
867        // Date/Time types
868        s if s.eq_ignore_ascii_case("date") => "chrono::NaiveDate",
869        s if s.eq_ignore_ascii_case("time") => "chrono::NaiveTime",
870        s if s.eq_ignore_ascii_case("timestamp") => "chrono::NaiveDateTime",
871        s if s.eq_ignore_ascii_case("timestamptz") => "chrono::DateTime<chrono::Utc>",
872
873        // JSON
874        s if s.eq_ignore_ascii_case("json") || s.eq_ignore_ascii_case("jsonb") => {
875            "serde_json::Value"
876        }
877
878        // Default to String for unknown types
879        _ => "String",
880    };
881
882    if not_null {
883        base_type.to_string()
884    } else {
885        format!("Option<{base_type}>")
886    }
887}
888
889/// Convert `PostgreSQL` type and array dimensions to a Rust type.
890#[must_use]
891pub fn sql_type_to_rust_type_with_dimensions(
892    sql_type: &str,
893    dimensions: Option<i32>,
894    not_null: bool,
895) -> String {
896    let Some(dimensions) = dimensions.filter(|dims| *dims > 0) else {
897        return sql_type_to_rust_type(sql_type, not_null);
898    };
899
900    let mut base = sql_type_to_rust_type(sql_type.trim_start_matches('_'), true);
901    for _ in 0..dimensions {
902        base = format!("Vec<{base}>");
903    }
904
905    if not_null {
906        base
907    } else {
908        format!("Option<{base}>")
909    }
910}
911
912fn write_doc_comment(code: &mut String, indent: &str, comment: &str) {
913    for line in comment.lines() {
914        if line.is_empty() {
915            let _ = writeln!(code, "{indent}///");
916        } else {
917            let _ = writeln!(code, "{indent}/// {line}");
918        }
919    }
920}
921
922/// Generate an index struct
923fn generate_index_struct(index: &Index, use_pub: bool, field_casing: FieldCasing) -> String {
924    let struct_name = index.name.to_pascal_case();
925    let table_name = index.table.to_pascal_case();
926    let vis = if use_pub { "pub " } else { "" };
927
928    let mut code = String::new();
929
930    let mut attrs = Vec::new();
931    if index.is_unique {
932        attrs.push("unique".to_string());
933    }
934    if index.concurrently {
935        attrs.push("concurrent".to_string());
936    }
937    if let Some(method) = &index.method
938        && !method.eq_ignore_ascii_case("btree")
939    {
940        attrs.push(format!("method = \"{}\"", escape_for_rust_literal(method)));
941    }
942    if let Some(where_clause) = &index.where_clause {
943        attrs.push(format!(
944            "where = \"{}\"",
945            escape_for_rust_literal(where_clause)
946        ));
947    }
948    if attrs.is_empty() {
949        code.push_str("#[PostgresIndex]\n");
950    } else {
951        let _ = writeln!(code, "#[PostgresIndex({})]", attrs.join(", "));
952    }
953
954    // Tuple struct with column references
955    let columns: Vec<String> = index
956        .columns
957        .iter()
958        .map(|c| {
959            if c.is_expression {
960                format!("\"{}\"", c.value) // Expression indexes use string literals
961            } else {
962                format!(
963                    "{}::{}",
964                    table_name,
965                    apply_field_casing(c.value.as_ref(), field_casing)
966                )
967            }
968        })
969        .collect();
970
971    let _ = writeln!(code, "{vis}struct {struct_name}({});", columns.join(", "));
972    code
973}
974
975/// Generate a view struct
976fn generate_view_struct(
977    view: &View,
978    columns: &[&Column],
979    enum_map: &HashMap<(String, String), String>,
980    use_pub: bool,
981    field_casing: FieldCasing,
982) -> String {
983    let struct_name = view.name.to_pascal_case();
984    let vis = if use_pub { "pub " } else { "" };
985
986    let mut code = String::new();
987
988    // Build view attributes
989    let mut attrs = Vec::new();
990
991    // Check if view name differs from struct name (snake_case version)
992    if apply_field_casing(&struct_name, field_casing) != view.name.as_ref() {
993        attrs.push(format!("name = \"{}\"", view.name));
994    }
995
996    // Add schema if not public
997    if view.schema != "public" {
998        attrs.push(format!("schema = \"{}\"", view.schema));
999    }
1000
1001    // Add materialized flag if true
1002    if view.materialized {
1003        attrs.push("materialized".to_string());
1004    }
1005
1006    // Add WITH NO DATA for materialized views
1007    if view.with_no_data == Some(true) {
1008        attrs.push("with_no_data".to_string());
1009    }
1010
1011    // Add USING clause for materialized views
1012    if let Some(using) = &view.using {
1013        attrs.push(format!("using = \"{using}\""));
1014    }
1015
1016    // Add TABLESPACE for materialized views
1017    if let Some(tablespace) = &view.tablespace {
1018        attrs.push(format!("tablespace = \"{tablespace}\""));
1019    }
1020
1021    // Add definition
1022    if let Some(def) = &view.definition {
1023        let escaped_def = escape_for_rust_literal(def);
1024        attrs.push(format!("definition = \"{escaped_def}\""));
1025    }
1026
1027    // Build the attribute line
1028    if attrs.is_empty() {
1029        code.push_str("#[PostgresView]\n");
1030    } else {
1031        let _ = writeln!(code, "#[PostgresView({})]", attrs.join(", "));
1032    }
1033
1034    // Struct definition with column fields
1035    let _ = writeln!(code, "{vis}struct {struct_name} {{");
1036
1037    // Sort columns by ordinal position
1038    let mut sorted_columns: Vec<&&Column> = columns.iter().collect();
1039    sorted_columns.sort_by(|a, b| {
1040        let ao = a.ordinal_position.unwrap_or(i32::MAX);
1041        let bo = b.ordinal_position.unwrap_or(i32::MAX);
1042        ao.cmp(&bo).then_with(|| a.name.cmp(&b.name))
1043    });
1044
1045    // Generate fields for each column
1046    for column in sorted_columns {
1047        let field_name = apply_field_casing(column.name.as_ref(), field_casing);
1048
1049        // Check if this column uses an enum type
1050        let type_schema = column.type_schema.as_deref().unwrap_or(&column.schema);
1051        let enum_type = enum_map.get(&(type_schema.to_string(), column.sql_type.to_string()));
1052
1053        // Determine Rust type - use enum type if available, otherwise map SQL type
1054        let rust_type = enum_type.map_or_else(
1055            || {
1056                sql_type_to_rust_type_with_dimensions(
1057                    &column.sql_type,
1058                    column.dimensions,
1059                    column.not_null,
1060                )
1061            },
1062            |enum_name| {
1063                if column.not_null {
1064                    enum_name.clone()
1065                } else {
1066                    format!("Option<{enum_name}>")
1067                }
1068            },
1069        );
1070
1071        let _ = writeln!(code, "    {vis}{field_name}: {rust_type},");
1072    }
1073
1074    code.push_str("}\n");
1075    code
1076}
1077
1078fn generate_policy_struct(policy: &Policy, use_pub: bool) -> String {
1079    let mut struct_name = policy.name.to_pascal_case();
1080    if struct_name.is_empty() {
1081        struct_name = "Policy".to_string();
1082    }
1083    let table_type = policy.table.to_pascal_case();
1084    let vis = if use_pub { "pub " } else { "" };
1085
1086    let mut attrs = Vec::new();
1087    if struct_name.to_snake_case() != policy.name.as_ref() {
1088        attrs.push(format!(
1089            "name = \"{}\"",
1090            escape_for_rust_literal(&policy.name)
1091        ));
1092    }
1093    if let Some(as_clause) = &policy.as_clause {
1094        attrs.push(format!("as = \"{}\"", escape_for_rust_literal(as_clause)));
1095    }
1096    if let Some(for_clause) = &policy.for_clause {
1097        attrs.push(format!("for = \"{}\"", escape_for_rust_literal(for_clause)));
1098    }
1099    if let Some(roles) = &policy.to
1100        && !roles.is_empty()
1101    {
1102        let roles = roles
1103            .iter()
1104            .map(|role| format!("\"{}\"", escape_for_rust_literal(role)))
1105            .collect::<Vec<_>>()
1106            .join(", ");
1107        attrs.push(format!("to({roles})"));
1108    }
1109    if let Some(using) = &policy.using {
1110        attrs.push(format!("using = \"{}\"", escape_for_rust_literal(using)));
1111    }
1112    if let Some(with_check) = &policy.with_check {
1113        attrs.push(format!(
1114            "with_check = \"{}\"",
1115            escape_for_rust_literal(with_check)
1116        ));
1117    }
1118
1119    let mut code = String::new();
1120    if attrs.is_empty() {
1121        code.push_str("#[PostgresPolicy]\n");
1122    } else {
1123        let _ = writeln!(code, "#[PostgresPolicy({})]", attrs.join(", "));
1124    }
1125    let _ = writeln!(code, "{vis}struct {struct_name}({table_type});");
1126    code
1127}
1128
1129/// Generate a schema struct
1130fn generate_schema_struct(
1131    schema_name: &str,
1132    tables: &[String],
1133    indexes: &[String],
1134    policies: &[String],
1135    use_pub: bool,
1136    field_casing: FieldCasing,
1137) -> String {
1138    let vis = if use_pub { "pub " } else { "" };
1139
1140    let mut code = String::new();
1141
1142    // Schema derive
1143    code.push_str("#[derive(PostgresSchema)]\n");
1144    let _ = writeln!(code, "{vis}struct {schema_name} {{");
1145
1146    // Table fields
1147    for table in tables {
1148        let field_name = apply_field_casing(table, field_casing);
1149        let type_name = table.to_pascal_case();
1150        let _ = writeln!(code, "    {vis}{field_name}: {type_name},");
1151    }
1152
1153    // Index fields (commented as they're typically not needed in schema)
1154    if !indexes.is_empty() {
1155        code.push_str("    // Indexes:\n");
1156        for index in indexes {
1157            let field_name = apply_field_casing(index, field_casing);
1158            let type_name = index.to_pascal_case();
1159            let _ = writeln!(code, "    // {field_name}: {type_name},");
1160        }
1161    }
1162
1163    for policy in policies {
1164        let field_name = apply_field_casing(policy, field_casing);
1165        let type_name = policy.to_pascal_case();
1166        let _ = writeln!(code, "    {vis}{field_name}: {type_name},");
1167    }
1168
1169    code.push_str("}\n");
1170    code
1171}
1172
1173#[cfg(test)]
1174mod tests {
1175    use super::*;
1176
1177    #[test]
1178    fn test_sql_type_to_rust_type() {
1179        assert_eq!(sql_type_to_rust_type("int4", true), "i32");
1180        assert_eq!(sql_type_to_rust_type("int8", true), "i64");
1181        assert_eq!(sql_type_to_rust_type("text", true), "String");
1182        assert_eq!(sql_type_to_rust_type("bool", true), "bool");
1183        assert_eq!(sql_type_to_rust_type("bytea", true), "Vec<u8>");
1184
1185        // Nullable types
1186        assert_eq!(sql_type_to_rust_type("int4", false), "Option<i32>");
1187        assert_eq!(sql_type_to_rust_type("text", false), "Option<String>");
1188    }
1189
1190    #[test]
1191    fn test_format_default_value() {
1192        // Numeric
1193        assert_eq!(format_default_value("42", "int4"), Some("42".to_string()));
1194        assert_eq!(
1195            format_default_value("3.14::numeric", "numeric"),
1196            Some("3.14".to_string())
1197        );
1198
1199        // Boolean
1200        assert_eq!(
1201            format_default_value("true", "bool"),
1202            Some("true".to_string())
1203        );
1204
1205        // String
1206        assert_eq!(
1207            format_default_value("'hello'::text", "text"),
1208            Some("\"hello\"".to_string())
1209        );
1210
1211        // Function calls should be None
1212        assert_eq!(format_default_value("now()", "timestamp"), None);
1213        assert_eq!(
1214            format_default_value("nextval('seq'::regclass)", "int4"),
1215            None
1216        );
1217    }
1218}