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 = format!("CREATE TABLE IF NOT EXISTS {} (\n", table.name);
55
56    let col_count = table.columns.len();
57    for (i, col) in table.columns.iter().enumerate() {
58        sql.push_str("    ");
59        sql.push_str(&col.name);
60        sql.push(' ');
61        sql.push_str(&sqlite_type(&col.col_type));
62
63        if col.primary_key {
64            sql.push_str(" PRIMARY KEY");
65            if let Some(ref default) = col.default
66                && let Some(mapped) = sqlite_default(default)
67            {
68                sql.push_str(" DEFAULT ");
69                sql.push_str(&mapped);
70            }
71            if let Some(ref fk) = col.foreign_key {
72                sql.push_str(&format!(
73                    " REFERENCES {}({}) {}",
74                    fk.references_table,
75                    fk.references_column,
76                    sqlite_on_delete(&fk.on_delete)
77                ));
78            }
79        } else if let Some(ref fk) = col.foreign_key {
80            if !col.nullable {
81                sql.push_str(" NOT NULL");
82            }
83            sql.push_str(&format!(
84                " REFERENCES {}({}) {}",
85                fk.references_table,
86                fk.references_column,
87                sqlite_on_delete(&fk.on_delete)
88            ));
89            if col.unique {
90                sql.push_str(" UNIQUE");
91            }
92        } else {
93            if !col.nullable {
94                sql.push_str(" NOT NULL");
95            }
96            if col.unique {
97                sql.push_str(" UNIQUE");
98            }
99            if let Some(ref default) = col.default
100                && let Some(mapped) = sqlite_default(default)
101            {
102                sql.push_str(" DEFAULT ");
103                sql.push_str(&mapped);
104            }
105        }
106
107        if i < col_count - 1 {
108            sql.push(',');
109        }
110        sql.push('\n');
111    }
112
113    sql.push_str(");\n");
114    sql
115}
116
117/// Generate complete SQLite DDL for the entire schema.
118///
119/// Returns one string with `PRAGMA foreign_keys = ON` followed by all
120/// `CREATE TABLE IF NOT EXISTS` statements in topological order.
121pub fn generate_sqlite_ddl(schema: &YAuthSchema) -> String {
122    let mut ddl = String::from("PRAGMA foreign_keys = ON;\n\n");
123    for (i, table) in schema.tables.iter().enumerate() {
124        if i > 0 {
125            ddl.push('\n');
126        }
127        ddl.push_str(&generate_create_table(table));
128    }
129    ddl
130}
131
132/// Generate DROP TABLE IF EXISTS statement for a single table (SQLite).
133pub fn generate_sqlite_drop(table: &TableDef) -> String {
134    format!("DROP TABLE IF EXISTS {};\n", table.name)
135}
136
137/// Generate DROP TABLE statements for a list of tables in reverse order (SQLite).
138pub fn generate_sqlite_drops(tables: &[TableDef]) -> String {
139    let mut ddl = String::new();
140    for (i, table) in tables.iter().rev().enumerate() {
141        if i > 0 {
142            ddl.push('\n');
143        }
144        ddl.push_str(&generate_sqlite_drop(table));
145    }
146    ddl
147}