Skip to main content

yauth_migration/
sqlite.rs

1//! SQLite DDL generation.
2//!
3//! Maps abstract column types to SQLite-compatible types:
4//! - UUID -> TEXT
5//! - VARCHAR / VarcharN(n) -> TEXT (SQLite has no VARCHAR length limits)
6//! - BOOLEAN -> INTEGER
7//! - DateTime -> TEXT (ISO 8601 strings)
8//! - Json -> TEXT
9//! - INT -> INTEGER
10//! - SMALLINT -> INTEGER
11//! - TEXT -> TEXT
12
13use std::borrow::Cow;
14
15use super::collector::YAuthSchema;
16use super::types::*;
17
18/// Map abstract column type to SQLite type string.
19pub(crate) fn sqlite_type(col_type: &ColumnType) -> Cow<'static, str> {
20    match col_type {
21        ColumnType::Uuid => Cow::Borrowed("TEXT"),
22        ColumnType::Varchar => Cow::Borrowed("TEXT"),
23        ColumnType::VarcharN(_) => Cow::Borrowed("TEXT"),
24        ColumnType::Boolean => Cow::Borrowed("INTEGER"),
25        ColumnType::DateTime => Cow::Borrowed("TEXT"),
26        ColumnType::Json => Cow::Borrowed("TEXT"),
27        ColumnType::Int => Cow::Borrowed("INTEGER"),
28        ColumnType::SmallInt => Cow::Borrowed("INTEGER"),
29        ColumnType::Text => Cow::Borrowed("TEXT"),
30    }
31}
32
33/// Map OnDelete action to SQLite clause.
34fn sqlite_on_delete(action: &OnDelete) -> &'static str {
35    match action {
36        OnDelete::Cascade => "ON DELETE CASCADE",
37        OnDelete::SetNull => "ON DELETE SET NULL",
38        OnDelete::Restrict => "ON DELETE RESTRICT",
39        OnDelete::NoAction => "ON DELETE NO ACTION",
40    }
41}
42
43/// Map a Postgres default expression to its SQLite equivalent.
44pub(crate) fn sqlite_default(pg_default: &str) -> Option<Cow<'static, str>> {
45    match pg_default {
46        "gen_random_uuid()" => None,
47        "now()" => Some(Cow::Borrowed("CURRENT_TIMESTAMP")),
48        other => Some(Cow::Owned(other.to_string())),
49    }
50}
51
52/// Generate a CREATE TABLE IF NOT EXISTS statement for a single table (SQLite).
53fn generate_create_table(table: &TableDef) -> String {
54    let mut sql = String::new();
55    if let Some(ref desc) = table.description {
56        sql.push_str(&format!("-- {desc}\n"));
57    }
58    sql.push_str(&format!("CREATE TABLE IF NOT EXISTS {} (\n", table.name));
59
60    let col_count = table.columns.len();
61    for (i, col) in table.columns.iter().enumerate() {
62        sql.push_str("    ");
63        sql.push_str(&col.name);
64        sql.push(' ');
65        sql.push_str(&sqlite_type(&col.col_type));
66
67        if col.primary_key {
68            sql.push_str(" PRIMARY KEY");
69            if let Some(ref default) = col.default
70                && let Some(mapped) = sqlite_default(default)
71            {
72                sql.push_str(" DEFAULT ");
73                sql.push_str(&mapped);
74            }
75            if let Some(ref fk) = col.foreign_key {
76                sql.push_str(&format!(
77                    " REFERENCES {}({}) {}",
78                    fk.references_table,
79                    fk.references_column,
80                    sqlite_on_delete(&fk.on_delete)
81                ));
82            }
83        } else if let Some(ref fk) = col.foreign_key {
84            if !col.nullable {
85                sql.push_str(" NOT NULL");
86            }
87            sql.push_str(&format!(
88                " REFERENCES {}({}) {}",
89                fk.references_table,
90                fk.references_column,
91                sqlite_on_delete(&fk.on_delete)
92            ));
93            if col.unique {
94                sql.push_str(" UNIQUE");
95            }
96        } else {
97            if !col.nullable {
98                sql.push_str(" NOT NULL");
99            }
100            if col.unique {
101                sql.push_str(" UNIQUE");
102            }
103            if let Some(ref default) = col.default
104                && let Some(mapped) = sqlite_default(default)
105            {
106                sql.push_str(" DEFAULT ");
107                sql.push_str(&mapped);
108            }
109        }
110
111        if i < col_count - 1 {
112            sql.push(',');
113        }
114        sql.push('\n');
115    }
116
117    sql.push_str(");\n");
118    sql
119}
120
121/// Generate complete SQLite DDL for the entire schema.
122///
123/// Returns one string with `PRAGMA foreign_keys = ON` followed by all
124/// `CREATE TABLE IF NOT EXISTS` statements in topological order.
125pub fn generate_sqlite_ddl(schema: &YAuthSchema) -> String {
126    let mut ddl = String::from("PRAGMA foreign_keys = ON;\n\n");
127    for (i, table) in schema.tables.iter().enumerate() {
128        if i > 0 {
129            ddl.push('\n');
130        }
131        ddl.push_str(&generate_create_table(table));
132    }
133    ddl
134}
135
136/// Generate DROP TABLE IF EXISTS statement for a single table (SQLite).
137pub fn generate_sqlite_drop(table: &TableDef) -> String {
138    format!("DROP TABLE IF EXISTS {};\n", table.name)
139}
140
141/// Generate DROP TABLE statements for a list of tables in reverse order (SQLite).
142pub fn generate_sqlite_drops(tables: &[TableDef]) -> String {
143    let mut ddl = String::new();
144    for (i, table) in tables.iter().rev().enumerate() {
145        if i > 0 {
146            ddl.push('\n');
147        }
148        ddl.push_str(&generate_sqlite_drop(table));
149    }
150    ddl
151}