Skip to main content

ruest_db_codegen/
sql.rs

1use ruest_db_schema::{Attribute, FieldKind, Schema};
2
3use crate::naming::{column_name, default_sql, pg_type, table_columns, table_name};
4
5/// SQL `CREATE TABLE` pour tous les modèles (ordre du schema).
6pub fn generate_create_all(schema: &Schema) -> String {
7    let mut parts = Vec::new();
8    for model in &schema.models {
9        parts.push(generate_create_table(model));
10    }
11    parts.join("\n\n")
12}
13
14pub fn generate_migration_sql(schema: &Schema) -> String {
15    format!(
16        "-- RuestDB migration\n-- Generated from schema.ruest\n\n{}\n",
17        generate_create_all(schema)
18    )
19}
20
21fn generate_create_table(model: &ruest_db_schema::Model) -> String {
22    let table = table_name(&model.name);
23    let mut lines = Vec::new();
24
25    for field in table_columns(model) {
26        let col = column_name(&field.name);
27        let mut line = format!("  \"{col}\" {}", pg_type(field));
28        if field
29            .attributes
30            .iter()
31            .any(|a| matches!(a, Attribute::Id))
32        {
33            line.push_str(" PRIMARY KEY");
34        }
35        if field
36            .attributes
37            .iter()
38            .any(|a| matches!(a, Attribute::Unique))
39        {
40            line.push_str(" UNIQUE");
41        }
42        if !field.optional {
43            line.push_str(" NOT NULL");
44        }
45        if let Some(def) = default_sql(field) {
46            line.push_str(&format!(" DEFAULT {def}"));
47        }
48        lines.push(line);
49    }
50
51    // FK depuis @relation (champ relationnel → colonne FK déjà déclarée)
52    for field in &model.fields {
53        let FieldKind::Model(target) = &field.kind else {
54            continue;
55        };
56        let Some(rel) = field.attributes.iter().find_map(|a| match a {
57            Attribute::Relation(r) => Some(r),
58            _ => None,
59        }) else {
60            continue;
61        };
62        let fk_col = column_name(&rel.fields[0]);
63        let ref_table = table_name(target);
64        let ref_col = column_name(&rel.references[0]);
65        lines.push(format!(
66            "  CONSTRAINT \"fk_{table}_{fk_col}\" FOREIGN KEY (\"{fk_col}\") REFERENCES \"{ref_table}\" (\"{ref_col}\") ON DELETE CASCADE"
67        ));
68    }
69
70    format!(
71        "CREATE TABLE IF NOT EXISTS \"{table}\" (\n{}\n);",
72        lines.join(",\n")
73    )
74}
75
76#[cfg(test)]
77mod tests {
78    use ruest_db_parser::parse_schema;
79    use super::*;
80
81    const SAMPLE: &str = r#"
82model User {
83  id    String @id @default(uuid())
84  email String @unique
85  name  String
86  posts Post[]
87}
88
89model Post {
90  id     String @id @default(uuid())
91  title  String
92  userId String
93  user   User @relation(fields: [userId], references: [id])
94}
95"#;
96
97    #[test]
98    fn generates_postgres_ddl() {
99        let schema = parse_schema(SAMPLE).unwrap();
100        let sql = generate_create_all(&schema);
101        assert!(sql.contains("CREATE TABLE IF NOT EXISTS \"users\""));
102        assert!(sql.contains("CREATE TABLE IF NOT EXISTS \"posts\""));
103        assert!(sql.contains("FOREIGN KEY"));
104    }
105}