1use 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#[derive(Debug, Clone, Default)]
18pub struct GeneratedSchema {
19 pub code: String,
21 pub enums: Vec<String>,
23 pub tables: Vec<String>,
25 pub indexes: Vec<String>,
27 pub views: Vec<String>,
29 pub policies: Vec<String>,
31 pub warnings: Vec<String>,
33}
34
35#[derive(Debug, Clone, Default)]
37pub struct CodegenOptions {
38 pub module_doc: Option<String>,
40 pub include_schema: bool,
42 pub schema_name: String,
44 pub use_pub: bool,
46 pub field_casing: FieldCasing,
48}
49
50#[derive(Debug, Clone, Copy, Default)]
52pub enum FieldCasing {
53 #[default]
55 Snake,
56 Camel,
58 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
89struct 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#[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 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 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 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 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
293struct 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
308fn 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 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 let _ = writeln!(code, "{vis}struct {struct_name} {{");
329
330 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 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
511fn 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
569fn 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
598fn 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 let should_add_primary = is_pk && !ctx.is_composite_pk;
613
614 let is_serial = column
616 .default
617 .as_ref()
618 .is_some_and(|d| d.contains("nextval"))
619 && column.identity.is_none();
620
621 let fk_info = ctx.fk_map.get(&(
623 column.schema.to_string(),
624 column.table.to_string(),
625 col_name_str,
626 ));
627
628 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 let mut attrs = Vec::new();
636
637 if is_serial {
639 attrs.push("serial".to_string());
640 }
641
642 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 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 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 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 if let Some((fk, idx)) = fk_info {
703 push_fk_attrs(&mut attrs, fk, *idx);
704 }
705
706 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 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
737fn 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 code.push_str("#[derive(PostgresEnum, Default, Clone, PartialEq, Debug)]\n");
747
748 let _ = writeln!(code, "{vis}enum {enum_name} {{");
750
751 for (idx, value) in e.values.iter().enumerate() {
753 let variant_name = value.to_pascal_case();
754 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
765fn 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 if default.contains('(') || default.starts_with("nextval") {
772 return None;
773 }
774
775 if default.eq_ignore_ascii_case("null") {
777 return None;
778 }
779
780 if default.eq_ignore_ascii_case("true") || default.eq_ignore_ascii_case("false") {
782 return Some(default.to_lowercase());
783 }
784
785 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 let value = default.split("::").next().unwrap_or(default);
794 return Some(value.trim_matches('\'').to_string());
795 }
796
797 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 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#[must_use]
814pub fn sql_type_to_rust_type(sql_type: &str, not_null: bool) -> String {
815 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 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 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", s if s.eq_ignore_ascii_case("bool") || s.eq_ignore_ascii_case("boolean") => "bool",
850
851 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 s if s.eq_ignore_ascii_case("bytea") => "Vec<u8>",
863
864 s if s.eq_ignore_ascii_case("uuid") => "uuid::Uuid",
866
867 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 s if s.eq_ignore_ascii_case("json") || s.eq_ignore_ascii_case("jsonb") => {
875 "serde_json::Value"
876 }
877
878 _ => "String",
880 };
881
882 if not_null {
883 base_type.to_string()
884 } else {
885 format!("Option<{base_type}>")
886 }
887}
888
889#[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
922fn 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 let columns: Vec<String> = index
956 .columns
957 .iter()
958 .map(|c| {
959 if c.is_expression {
960 format!("\"{}\"", c.value) } 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
975fn 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 let mut attrs = Vec::new();
990
991 if apply_field_casing(&struct_name, field_casing) != view.name.as_ref() {
993 attrs.push(format!("name = \"{}\"", view.name));
994 }
995
996 if view.schema != "public" {
998 attrs.push(format!("schema = \"{}\"", view.schema));
999 }
1000
1001 if view.materialized {
1003 attrs.push("materialized".to_string());
1004 }
1005
1006 if view.with_no_data == Some(true) {
1008 attrs.push("with_no_data".to_string());
1009 }
1010
1011 if let Some(using) = &view.using {
1013 attrs.push(format!("using = \"{using}\""));
1014 }
1015
1016 if let Some(tablespace) = &view.tablespace {
1018 attrs.push(format!("tablespace = \"{tablespace}\""));
1019 }
1020
1021 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 if attrs.is_empty() {
1029 code.push_str("#[PostgresView]\n");
1030 } else {
1031 let _ = writeln!(code, "#[PostgresView({})]", attrs.join(", "));
1032 }
1033
1034 let _ = writeln!(code, "{vis}struct {struct_name} {{");
1036
1037 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 for column in sorted_columns {
1047 let field_name = apply_field_casing(column.name.as_ref(), field_casing);
1048
1049 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 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
1129fn 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 code.push_str("#[derive(PostgresSchema)]\n");
1144 let _ = writeln!(code, "{vis}struct {schema_name} {{");
1145
1146 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 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 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 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 assert_eq!(
1201 format_default_value("true", "bool"),
1202 Some("true".to_string())
1203 );
1204
1205 assert_eq!(
1207 format_default_value("'hello'::text", "text"),
1208 Some("\"hello\"".to_string())
1209 );
1210
1211 assert_eq!(format_default_value("now()", "timestamp"), None);
1213 assert_eq!(
1214 format_default_value("nextval('seq'::regclass)", "int4"),
1215 None
1216 );
1217 }
1218}