oxide_sql_core/migrations/
snapshot.rs1use std::collections::BTreeMap;
9
10use crate::ast::DataType;
11use crate::schema::{RustTypeMapping, TableSchema};
12
13use super::column_builder::{DefaultValue, ForeignKeyAction};
14use super::operation::{IndexType, strip_option};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct IndexSnapshot {
19 pub name: String,
21 pub columns: Vec<String>,
23 pub unique: bool,
25 pub index_type: IndexType,
27 pub condition: Option<String>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct ForeignKeySnapshot {
34 pub name: Option<String>,
36 pub columns: Vec<String>,
38 pub references_table: String,
40 pub references_columns: Vec<String>,
42 pub on_delete: Option<ForeignKeyAction>,
44 pub on_update: Option<ForeignKeyAction>,
46}
47
48#[derive(Debug, Clone, PartialEq)]
50pub struct ColumnSnapshot {
51 pub name: String,
53 pub data_type: DataType,
55 pub nullable: bool,
57 pub primary_key: bool,
59 pub unique: bool,
61 pub autoincrement: bool,
63 pub default: Option<DefaultValue>,
65}
66
67#[derive(Debug, Clone, PartialEq)]
69pub struct TableSnapshot {
70 pub name: String,
72 pub columns: Vec<ColumnSnapshot>,
74 pub indexes: Vec<IndexSnapshot>,
76 pub foreign_keys: Vec<ForeignKeySnapshot>,
78}
79
80impl TableSnapshot {
81 pub fn from_table_schema<T: TableSchema>(dialect: &impl RustTypeMapping) -> Self {
85 let columns = T::SCHEMA
86 .iter()
87 .map(|col| {
88 let inner = strip_option(col.rust_type);
89 let data_type = dialect.map_type(inner);
90 let default = col
91 .default_expr
92 .map(|expr| DefaultValue::Expression(expr.to_string()));
93 ColumnSnapshot {
94 name: col.name.to_string(),
95 data_type,
96 nullable: col.nullable,
97 primary_key: col.primary_key,
98 unique: col.unique,
99 autoincrement: col.autoincrement,
100 default,
101 }
102 })
103 .collect();
104 Self {
105 name: T::NAME.to_string(),
106 columns,
107 indexes: vec![],
108 foreign_keys: vec![],
109 }
110 }
111
112 #[must_use]
114 pub fn column(&self, name: &str) -> Option<&ColumnSnapshot> {
115 self.columns.iter().find(|c| c.name == name)
116 }
117}
118
119#[derive(Debug, Clone, PartialEq)]
121pub struct SchemaSnapshot {
122 pub tables: BTreeMap<String, TableSnapshot>,
124}
125
126impl SchemaSnapshot {
127 #[must_use]
129 pub fn new() -> Self {
130 Self {
131 tables: BTreeMap::new(),
132 }
133 }
134
135 pub fn add_table(&mut self, table: TableSnapshot) {
137 self.tables.insert(table.name.clone(), table);
138 }
139
140 pub fn add_from_table_schema<T: TableSchema>(&mut self, dialect: &impl RustTypeMapping) {
142 let snapshot = TableSnapshot::from_table_schema::<T>(dialect);
143 self.add_table(snapshot);
144 }
145}
146
147impl Default for SchemaSnapshot {
148 fn default() -> Self {
149 Self::new()
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::migrations::{DuckDbDialect, PostgresDialect, SqliteDialect};
157 use crate::schema::{ColumnSchema, Table};
158
159 struct TestTable;
161 struct TestRow;
162
163 impl Table for TestTable {
164 type Row = TestRow;
165 const NAME: &'static str = "test_items";
166 const COLUMNS: &'static [&'static str] = &["id", "name", "score", "active"];
167 const PRIMARY_KEY: Option<&'static str> = Some("id");
168 }
169
170 impl TableSchema for TestTable {
171 const SCHEMA: &'static [ColumnSchema] = &[
172 ColumnSchema {
173 name: "id",
174 rust_type: "i64",
175 nullable: false,
176 primary_key: true,
177 unique: false,
178 autoincrement: true,
179 default_expr: None,
180 },
181 ColumnSchema {
182 name: "name",
183 rust_type: "String",
184 nullable: false,
185 primary_key: false,
186 unique: true,
187 autoincrement: false,
188 default_expr: None,
189 },
190 ColumnSchema {
191 name: "score",
192 rust_type: "Option<f64>",
193 nullable: true,
194 primary_key: false,
195 unique: false,
196 autoincrement: false,
197 default_expr: None,
198 },
199 ColumnSchema {
200 name: "active",
201 rust_type: "bool",
202 nullable: false,
203 primary_key: false,
204 unique: false,
205 autoincrement: false,
206 default_expr: Some("TRUE"),
207 },
208 ];
209 }
210
211 #[test]
212 fn from_table_schema_sqlite() {
213 let dialect = SqliteDialect::new();
214 let snap = TableSnapshot::from_table_schema::<TestTable>(&dialect);
215
216 assert_eq!(snap.name, "test_items");
217 assert_eq!(snap.columns.len(), 4);
218
219 let id = snap.column("id").unwrap();
220 assert_eq!(id.data_type, DataType::Bigint);
221 assert!(id.primary_key);
222 assert!(id.autoincrement);
223 assert!(!id.nullable);
224
225 let name_col = snap.column("name").unwrap();
226 assert_eq!(name_col.data_type, DataType::Text);
227 assert!(name_col.unique);
228
229 let score = snap.column("score").unwrap();
231 assert_eq!(score.data_type, DataType::Double);
232 assert!(score.nullable);
233
234 let active = snap.column("active").unwrap();
235 assert_eq!(active.data_type, DataType::Integer);
236 assert_eq!(
237 active.default,
238 Some(DefaultValue::Expression("TRUE".into()))
239 );
240 }
241
242 #[test]
243 fn from_table_schema_postgres() {
244 let dialect = PostgresDialect::new();
245 let snap = TableSnapshot::from_table_schema::<TestTable>(&dialect);
246
247 let name_col = snap.column("name").unwrap();
248 assert_eq!(name_col.data_type, DataType::Varchar(Some(255)));
249
250 let active = snap.column("active").unwrap();
251 assert_eq!(active.data_type, DataType::Boolean);
252 }
253
254 #[test]
255 fn from_table_schema_duckdb() {
256 let dialect = DuckDbDialect::new();
257 let snap = TableSnapshot::from_table_schema::<TestTable>(&dialect);
258
259 let name_col = snap.column("name").unwrap();
260 assert_eq!(name_col.data_type, DataType::Varchar(None));
261
262 let active = snap.column("active").unwrap();
263 assert_eq!(active.data_type, DataType::Boolean);
264 }
265
266 #[test]
267 fn column_lookup_by_name() {
268 let dialect = SqliteDialect::new();
269 let snap = TableSnapshot::from_table_schema::<TestTable>(&dialect);
270
271 assert!(snap.column("id").is_some());
272 assert!(snap.column("name").is_some());
273 assert!(snap.column("nonexistent").is_none());
274 }
275
276 #[test]
277 fn schema_snapshot_add_tables() {
278 let dialect = SqliteDialect::new();
279 let mut schema = SchemaSnapshot::new();
280 schema.add_from_table_schema::<TestTable>(&dialect);
281
282 assert_eq!(schema.tables.len(), 1);
283 assert!(schema.tables.contains_key("test_items"));
284 }
285}