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