Skip to main content

yauth_migration/
types.rs

1//! Core types for declarative schema definitions.
2
3/// Supported SQL dialects for DDL generation.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum Dialect {
6    Postgres,
7    Sqlite,
8    Mysql,
9}
10
11impl std::fmt::Display for Dialect {
12    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13        match self {
14            Dialect::Postgres => write!(f, "postgres"),
15            Dialect::Sqlite => write!(f, "sqlite"),
16            Dialect::Mysql => write!(f, "mysql"),
17        }
18    }
19}
20
21impl std::str::FromStr for Dialect {
22    type Err = String;
23    fn from_str(s: &str) -> Result<Self, Self::Err> {
24        match s.to_lowercase().as_str() {
25            "postgres" | "postgresql" | "pg" => Ok(Dialect::Postgres),
26            "sqlite" => Ok(Dialect::Sqlite),
27            "mysql" | "mariadb" => Ok(Dialect::Mysql),
28            _ => Err(format!("unknown dialect: '{s}'")),
29        }
30    }
31}
32
33/// Supported ORM formats for migration file generation.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
35#[serde(rename_all = "lowercase")]
36pub enum Orm {
37    Diesel,
38    Sqlx,
39    SeaOrm,
40}
41
42impl std::fmt::Display for Orm {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        match self {
45            Orm::Diesel => write!(f, "diesel"),
46            Orm::Sqlx => write!(f, "sqlx"),
47            Orm::SeaOrm => write!(f, "seaorm"),
48        }
49    }
50}
51
52impl std::str::FromStr for Orm {
53    type Err = String;
54    fn from_str(s: &str) -> Result<Self, Self::Err> {
55        match s.to_lowercase().as_str() {
56            "diesel" => Ok(Orm::Diesel),
57            "sqlx" => Ok(Orm::Sqlx),
58            "seaorm" | "sea-orm" | "sea_orm" => Ok(Orm::SeaOrm),
59            _ => Err(format!("unknown orm: '{s}'")),
60        }
61    }
62}
63
64/// Abstract column type -- each dialect maps this to a concrete SQL type.
65#[derive(Debug, Clone, PartialEq, Eq, Hash)]
66pub enum ColumnType {
67    /// UUID type. Postgres: UUID.
68    Uuid,
69    /// Variable-length string. Postgres: VARCHAR.
70    Varchar,
71    /// Variable-length string with max length. Postgres: VARCHAR(n).
72    VarcharN(u32),
73    /// Boolean. Postgres: BOOLEAN.
74    Boolean,
75    /// Timestamp with timezone. Postgres: TIMESTAMPTZ.
76    DateTime,
77    /// JSON binary. Postgres: JSONB.
78    Json,
79    /// 32-bit integer. Postgres: INT.
80    Int,
81    /// 16-bit integer. Postgres: SMALLINT.
82    SmallInt,
83    /// Text (unbounded). Postgres: TEXT.
84    Text,
85}
86
87/// ON DELETE action for foreign keys.
88#[derive(Debug, Clone, PartialEq, Eq, Hash)]
89pub enum OnDelete {
90    Cascade,
91    SetNull,
92    Restrict,
93    NoAction,
94}
95
96/// Foreign key reference from a column to another table's column.
97#[derive(Debug, Clone, PartialEq, Eq, Hash)]
98pub struct ForeignKey {
99    pub references_table: String,
100    pub references_column: String,
101    pub on_delete: OnDelete,
102}
103
104/// Definition of a single column.
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct ColumnDef {
107    pub name: String,
108    pub col_type: ColumnType,
109    pub nullable: bool,
110    pub primary_key: bool,
111    pub unique: bool,
112    pub default: Option<String>,
113    pub foreign_key: Option<ForeignKey>,
114}
115
116impl ColumnDef {
117    /// Create a new non-null column with no constraints.
118    pub fn new(name: &str, col_type: ColumnType) -> Self {
119        Self {
120            name: name.to_string(),
121            col_type,
122            nullable: false,
123            primary_key: false,
124            unique: false,
125            default: None,
126            foreign_key: None,
127        }
128    }
129
130    pub fn nullable(mut self) -> Self {
131        self.nullable = true;
132        self
133    }
134
135    pub fn primary_key(mut self) -> Self {
136        self.primary_key = true;
137        self
138    }
139
140    pub fn unique(mut self) -> Self {
141        self.unique = true;
142        self
143    }
144
145    pub fn default(mut self, val: &str) -> Self {
146        self.default = Some(val.to_string());
147        self
148    }
149
150    pub fn references(mut self, table: &str, column: &str, on_delete: OnDelete) -> Self {
151        self.foreign_key = Some(ForeignKey {
152            references_table: table.to_string(),
153            references_column: column.to_string(),
154            on_delete,
155        });
156        self
157    }
158}
159
160/// Index definition for a table.
161#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct IndexDef {
163    pub name: String,
164    pub columns: Vec<String>,
165    pub unique: bool,
166}
167
168/// Definition of a single table.
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct TableDef {
171    pub name: String,
172    pub columns: Vec<ColumnDef>,
173    pub indices: Vec<IndexDef>,
174}
175
176impl TableDef {
177    pub fn new(name: &str) -> Self {
178        Self {
179            name: name.to_string(),
180            columns: Vec::new(),
181            indices: Vec::new(),
182        }
183    }
184
185    pub fn column(mut self, col: ColumnDef) -> Self {
186        self.columns.push(col);
187        self
188    }
189
190    pub fn index(mut self, idx: IndexDef) -> Self {
191        self.indices.push(idx);
192        self
193    }
194
195    /// Get the names of tables this table depends on (via foreign keys).
196    pub fn dependencies(&self) -> Vec<&str> {
197        self.columns
198            .iter()
199            .filter_map(|c| {
200                c.foreign_key
201                    .as_ref()
202                    .map(|fk| fk.references_table.as_str())
203            })
204            .filter(|t| *t != self.name)
205            .collect()
206    }
207
208    /// Replace `old_prefix` with `new_prefix` in the table name and all FK references.
209    pub fn apply_prefix(&mut self, old_prefix: &str, new_prefix: &str) {
210        if self.name.starts_with(old_prefix) {
211            self.name = format!("{}{}", new_prefix, &self.name[old_prefix.len()..]);
212        }
213        for col in &mut self.columns {
214            if let Some(ref mut fk) = col.foreign_key
215                && fk.references_table.starts_with(old_prefix)
216            {
217                fk.references_table =
218                    format!("{}{}", new_prefix, &fk.references_table[old_prefix.len()..]);
219            }
220        }
221        for idx in &mut self.indices {
222            if idx.name.starts_with(old_prefix) {
223                idx.name = format!("{}{}", new_prefix, &idx.name[old_prefix.len()..]);
224            }
225        }
226    }
227}