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