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 = format!("CREATE TABLE IF NOT EXISTS {} (\n", table.name);
36
37    let col_count = table.columns.len();
38    for (i, col) in table.columns.iter().enumerate() {
39        sql.push_str("    ");
40        sql.push_str(&col.name);
41        sql.push(' ');
42        sql.push_str(&pg_type(&col.col_type));
43
44        if col.primary_key {
45            sql.push_str(" PRIMARY KEY");
46            if let Some(ref default) = col.default {
47                sql.push_str(" DEFAULT ");
48                sql.push_str(default);
49            }
50            // PK can also have a FK reference (e.g., yauth_passwords.user_id)
51            if let Some(ref fk) = col.foreign_key {
52                sql.push_str(&format!(
53                    " REFERENCES {}({}) {}",
54                    fk.references_table,
55                    fk.references_column,
56                    pg_on_delete(&fk.on_delete)
57                ));
58            }
59        } else if let Some(ref fk) = col.foreign_key {
60            // FK columns: [NOT NULL] REFERENCES ... [UNIQUE]
61            if !col.nullable {
62                sql.push_str(" NOT NULL");
63            }
64            sql.push_str(&format!(
65                " REFERENCES {}({}) {}",
66                fk.references_table,
67                fk.references_column,
68                pg_on_delete(&fk.on_delete)
69            ));
70            if col.unique {
71                sql.push_str(" UNIQUE");
72            }
73        } else {
74            if !col.nullable {
75                sql.push_str(" NOT NULL");
76            }
77            if col.unique {
78                sql.push_str(" UNIQUE");
79            }
80            if let Some(ref default) = col.default {
81                sql.push_str(" DEFAULT ");
82                sql.push_str(default);
83            }
84        }
85
86        if i < col_count - 1 {
87            sql.push(',');
88        }
89        sql.push('\n');
90    }
91
92    sql.push_str(");\n");
93    sql
94}
95
96/// Generate complete Postgres DDL for the entire schema.
97///
98/// Returns one string with all CREATE TABLE IF NOT EXISTS statements,
99/// in topological order (dependencies first).
100pub fn generate_postgres_ddl(schema: &YAuthSchema) -> String {
101    let mut ddl = String::new();
102    for (i, table) in schema.tables.iter().enumerate() {
103        if i > 0 {
104            ddl.push('\n');
105        }
106        ddl.push_str(&generate_create_table(table));
107    }
108    ddl
109}
110
111/// Generate a DROP TABLE IF EXISTS statement for a single table.
112pub fn generate_postgres_drop(table: &TableDef) -> String {
113    format!("DROP TABLE IF EXISTS {} CASCADE;\n", table.name)
114}
115
116/// Generate DROP TABLE statements for a list of tables in reverse order.
117pub fn generate_postgres_drops(tables: &[TableDef]) -> String {
118    let mut ddl = String::new();
119    for (i, table) in tables.iter().rev().enumerate() {
120        if i > 0 {
121            ddl.push('\n');
122        }
123        ddl.push_str(&generate_postgres_drop(table));
124    }
125    ddl
126}