Skip to main content

yauth_migration/
postgres.rs

1//! Postgres DDL generation.
2
3use super::collector::YAuthSchema;
4use super::types::*;
5
6use std::borrow::Cow;
7
8/// Map abstract column type to Postgres type string.
9pub(crate) fn pg_type(col_type: &ColumnType) -> Cow<'static, str> {
10    match col_type {
11        ColumnType::Uuid => Cow::Borrowed("UUID"),
12        ColumnType::Varchar => Cow::Borrowed("VARCHAR"),
13        ColumnType::VarcharN(n) => Cow::Owned(format!("VARCHAR({n})")),
14        ColumnType::Boolean => Cow::Borrowed("BOOLEAN"),
15        ColumnType::DateTime => Cow::Borrowed("TIMESTAMPTZ"),
16        ColumnType::Json => Cow::Borrowed("JSONB"),
17        ColumnType::Int => Cow::Borrowed("INT"),
18        ColumnType::SmallInt => Cow::Borrowed("SMALLINT"),
19        ColumnType::Text => Cow::Borrowed("TEXT"),
20    }
21}
22
23/// Map OnDelete action to Postgres clause.
24fn pg_on_delete(action: &OnDelete) -> &'static str {
25    match action {
26        OnDelete::Cascade => "ON DELETE CASCADE",
27        OnDelete::SetNull => "ON DELETE SET NULL",
28        OnDelete::Restrict => "ON DELETE RESTRICT",
29        OnDelete::NoAction => "ON DELETE NO ACTION",
30    }
31}
32
33/// Generate a CREATE TABLE IF NOT EXISTS statement for a single table.
34fn generate_create_table(table: &TableDef) -> String {
35    let mut sql = String::new();
36    if let Some(ref desc) = table.description {
37        sql.push_str(&format!("-- {desc}\n"));
38    }
39    sql.push_str(&format!("CREATE TABLE IF NOT EXISTS {} (\n", table.name));
40
41    let col_count = table.columns.len();
42    for (i, col) in table.columns.iter().enumerate() {
43        sql.push_str("    ");
44        sql.push_str(&col.name);
45        sql.push(' ');
46        sql.push_str(&pg_type(&col.col_type));
47
48        if col.primary_key {
49            sql.push_str(" PRIMARY KEY");
50            if let Some(ref default) = col.default {
51                sql.push_str(" DEFAULT ");
52                sql.push_str(default);
53            }
54            // PK can also have a FK reference (e.g., yauth_passwords.user_id)
55            if let Some(ref fk) = col.foreign_key {
56                sql.push_str(&format!(
57                    " REFERENCES {}({}) {}",
58                    fk.references_table,
59                    fk.references_column,
60                    pg_on_delete(&fk.on_delete)
61                ));
62            }
63        } else if let Some(ref fk) = col.foreign_key {
64            // FK columns: [NOT NULL] REFERENCES ... [UNIQUE]
65            if !col.nullable {
66                sql.push_str(" NOT NULL");
67            }
68            sql.push_str(&format!(
69                " REFERENCES {}({}) {}",
70                fk.references_table,
71                fk.references_column,
72                pg_on_delete(&fk.on_delete)
73            ));
74            if col.unique {
75                sql.push_str(" UNIQUE");
76            }
77        } else {
78            if !col.nullable {
79                sql.push_str(" NOT NULL");
80            }
81            if col.unique {
82                sql.push_str(" UNIQUE");
83            }
84            if let Some(ref default) = col.default {
85                sql.push_str(" DEFAULT ");
86                sql.push_str(default);
87            }
88        }
89
90        if i < col_count - 1 {
91            sql.push(',');
92        }
93        sql.push('\n');
94    }
95
96    sql.push_str(");\n");
97    sql
98}
99
100/// Generate complete Postgres DDL for the entire schema.
101///
102/// Returns one string with all CREATE TABLE IF NOT EXISTS statements,
103/// in topological order (dependencies first).
104pub fn generate_postgres_ddl(schema: &YAuthSchema) -> String {
105    let mut ddl = String::new();
106    for (i, table) in schema.tables.iter().enumerate() {
107        if i > 0 {
108            ddl.push('\n');
109        }
110        ddl.push_str(&generate_create_table(table));
111    }
112    ddl
113}
114
115/// Generate a DROP TABLE IF EXISTS statement for a single table.
116pub fn generate_postgres_drop(table: &TableDef) -> String {
117    format!("DROP TABLE IF EXISTS {} CASCADE;\n", table.name)
118}
119
120/// Generate DROP TABLE statements for a list of tables in reverse order.
121pub fn generate_postgres_drops(tables: &[TableDef]) -> String {
122    let mut ddl = String::new();
123    for (i, table) in tables.iter().rev().enumerate() {
124        if i > 0 {
125            ddl.push('\n');
126        }
127        ddl.push_str(&generate_postgres_drop(table));
128    }
129    ddl
130}