Skip to main content

rust_ef/migration/
snapshot_io.rs

1//! Model snapshot JSON serialization / deserialization.
2
3use super::types::{
4    ModelSnapshot, SnapshotCheck, SnapshotColumn, SnapshotEntityType, SnapshotForeignKey,
5    SnapshotIndex, SnapshotIndexColumn, SnapshotTableOptions,
6};
7use crate::error::{EFError, EFResult};
8use serde_json::{json, Map, Value};
9
10/// Parses a model snapshot JSON file (same format as
11/// [`super::history::MigrationStore::save_snapshot`]).
12pub fn parse_model_snapshot_json(text: &str) -> EFResult<Option<ModelSnapshot>> {
13    snapshot_from_json(text)
14}
15
16pub(super) fn migration_io_err(e: std::io::Error) -> EFError {
17    EFError::migration(e.to_string())
18}
19
20pub(super) fn snapshot_to_json(snapshot: &ModelSnapshot) -> String {
21    let entity_types: Vec<Value> = snapshot
22        .entity_types
23        .iter()
24        .map(|et| {
25            let columns: Vec<Value> = et
26                .columns
27                .iter()
28                .map(|c| {
29                    json!({
30                        "field_name": c.field_name,
31                        "column_name": c.column_name,
32                        "type_name": c.type_name,
33                        "is_primary_key": c.is_primary_key,
34                        "is_required": c.is_required,
35                        "is_foreign_key": c.is_foreign_key,
36                        "max_length": c.max_length,
37                        "is_auto_increment": c.is_auto_increment,
38                        "is_sequence": c.is_sequence,
39                        "sequence_name": c.sequence_name,
40                        "fk_referenced_table": c.fk_referenced_table,
41                        "fk_referenced_column": c.fk_referenced_column,
42                        "has_index": c.has_index,
43                        "is_unique": c.is_unique,
44                        "fk_on_delete": c.fk_on_delete,
45                        "default_value": c.default_value,
46                        "column_type": c.column_type,
47                        "precision": c.precision,
48                        "scale": c.scale,
49                        "is_fixed_length": c.is_fixed_length,
50                        "collation": c.collation,
51                        "comment": c.comment,
52                        "computed_sql": c.computed_sql,
53                        "computed_stored": c.computed_stored,
54                    })
55                })
56                .collect();
57            let indexes: Vec<Value> = et
58                .indexes
59                .iter()
60                .map(|i| {
61                    json!({
62                        "name": i.name,
63                        "columns": i
64                            .columns
65                            .iter()
66                            .map(|c| json!({"value": c.value, "is_expression": c.is_expression}))
67                            .collect::<Vec<_>>(),
68                        "is_unique": i.is_unique,
69                        "filter": i.filter,
70                        "include": i.include,
71                    })
72                })
73                .collect();
74            let foreign_keys: Vec<Value> = et
75                .foreign_keys
76                .iter()
77                .map(|f| {
78                    json!({
79                        "name": f.name,
80                        "columns": f.columns,
81                        "referenced_table": f.referenced_table,
82                        "referenced_columns": f.referenced_columns,
83                        "on_delete": f.on_delete,
84                    })
85                })
86                .collect();
87            let checks: Vec<Value> = et
88                .checks
89                .iter()
90                .map(|c| json!({"name": c.name, "expression": c.expression}))
91                .collect();
92            json!({
93                "type_name": et.type_name,
94                "table_name": et.table_name,
95                "columns": columns,
96                "indexes": indexes,
97                "foreign_keys": foreign_keys,
98                "checks": checks,
99                "table_options": {
100                    "engine": et.table_options.engine,
101                    "charset": et.table_options.charset,
102                    "collate": et.table_options.collate,
103                },
104                "is_view": et.is_view,
105                "is_keyless": et.is_keyless,
106                "comment": et.comment,
107            })
108        })
109        .collect();
110
111    serde_json::to_string_pretty(&json!({
112        "migration_id": snapshot.migration_id,
113        "entity_types": entity_types,
114    }))
115    .unwrap_or_else(|_| "{}".to_string())
116}
117
118fn snapshot_from_json(text: &str) -> EFResult<Option<ModelSnapshot>> {
119    let root: Value = match serde_json::from_str(text) {
120        Ok(v) => v,
121        Err(_) => return Ok(None),
122    };
123    let Some(entity_types) = root.get("entity_types").and_then(Value::as_array) else {
124        return Ok(None);
125    };
126
127    let migration_id = root
128        .get("migration_id")
129        .and_then(Value::as_str)
130        .unwrap_or("__snapshot__")
131        .to_string();
132
133    let mut types = Vec::with_capacity(entity_types.len());
134    for et in entity_types {
135        let Some(obj) = et.as_object() else {
136            continue;
137        };
138        let table_name = string_field(obj, "table_name");
139        if table_name.is_empty() {
140            continue;
141        }
142        let columns = obj
143            .get("columns")
144            .and_then(Value::as_array)
145            .map(|cols| {
146                cols.iter()
147                    .filter_map(Value::as_object)
148                    .map(|c| SnapshotColumn {
149                        field_name: string_field(c, "field_name"),
150                        column_name: {
151                            let name = string_field(c, "column_name");
152                            if name.is_empty() {
153                                string_field(c, "field_name")
154                            } else {
155                                name
156                            }
157                        },
158                        type_name: {
159                            let name = string_field(c, "type_name");
160                            if name.is_empty() {
161                                "String".to_string()
162                            } else {
163                                name
164                            }
165                        },
166                        is_primary_key: bool_field(c, "is_primary_key"),
167                        is_required: bool_field(c, "is_required"),
168                        is_foreign_key: bool_field(c, "is_foreign_key"),
169                        max_length: c
170                            .get("max_length")
171                            .and_then(Value::as_u64)
172                            .map(|n| n as usize),
173                        is_auto_increment: bool_field(c, "is_auto_increment"),
174                        is_sequence: bool_field(c, "is_sequence"),
175                        sequence_name: opt_string(c, "sequence_name"),
176                        fk_referenced_table: opt_string(c, "fk_referenced_table"),
177                        fk_referenced_column: opt_string(c, "fk_referenced_column"),
178                        has_index: bool_field(c, "has_index"),
179                        is_unique: bool_field(c, "is_unique"),
180                        fk_on_delete: opt_string(c, "fk_on_delete"),
181                        default_value: opt_string(c, "default_value"),
182                        column_type: opt_string(c, "column_type"),
183                        precision: c.get("precision").and_then(Value::as_u64).map(|n| n as u32),
184                        scale: c.get("scale").and_then(Value::as_u64).map(|n| n as u32),
185                        is_fixed_length: bool_field(c, "is_fixed_length"),
186                        collation: opt_string(c, "collation"),
187                        comment: opt_string(c, "comment"),
188                        computed_sql: opt_string(c, "computed_sql"),
189                        computed_stored: bool_field(c, "computed_stored"),
190                    })
191                    .collect()
192            })
193            .unwrap_or_default();
194
195        let indexes = obj
196            .get("indexes")
197            .and_then(Value::as_array)
198            .map(|items| {
199                items
200                    .iter()
201                    .filter_map(Value::as_object)
202                    .map(|i| SnapshotIndex {
203                        name: string_field(i, "name"),
204                        columns: i
205                            .get("columns")
206                            .and_then(Value::as_array)
207                            .map(|cols| {
208                                cols.iter()
209                                    .filter_map(Value::as_object)
210                                    .map(|c| SnapshotIndexColumn {
211                                        value: string_field(c, "value"),
212                                        is_expression: bool_field(c, "is_expression"),
213                                    })
214                                    .collect()
215                            })
216                            .unwrap_or_default(),
217                        is_unique: bool_field(i, "is_unique"),
218                        filter: opt_string(i, "filter"),
219                        include: string_array(i, "include"),
220                    })
221                    .collect()
222            })
223            .unwrap_or_default();
224
225        let foreign_keys = obj
226            .get("foreign_keys")
227            .and_then(Value::as_array)
228            .map(|items| {
229                items
230                    .iter()
231                    .filter_map(Value::as_object)
232                    .map(|f| SnapshotForeignKey {
233                        name: string_field(f, "name"),
234                        columns: string_array(f, "columns"),
235                        referenced_table: string_field(f, "referenced_table"),
236                        referenced_columns: string_array(f, "referenced_columns"),
237                        on_delete: opt_string(f, "on_delete"),
238                    })
239                    .collect()
240            })
241            .unwrap_or_default();
242
243        let checks = obj
244            .get("checks")
245            .and_then(Value::as_array)
246            .map(|items| {
247                items
248                    .iter()
249                    .filter_map(Value::as_object)
250                    .map(|c| SnapshotCheck {
251                        name: string_field(c, "name"),
252                        expression: string_field(c, "expression"),
253                    })
254                    .collect()
255            })
256            .unwrap_or_default();
257
258        let table_options = obj
259            .get("table_options")
260            .and_then(Value::as_object)
261            .map(|o| SnapshotTableOptions {
262                engine: opt_string(o, "engine"),
263                charset: opt_string(o, "charset"),
264                collate: opt_string(o, "collate"),
265            })
266            .unwrap_or_default();
267
268        types.push(SnapshotEntityType {
269            type_name: {
270                let name = string_field(obj, "type_name");
271                if name.is_empty() {
272                    table_name.clone()
273                } else {
274                    name
275                }
276            },
277            table_name,
278            columns,
279            indexes,
280            foreign_keys,
281            checks,
282            table_options,
283            is_view: bool_field(obj, "is_view"),
284            is_keyless: bool_field(obj, "is_keyless"),
285            comment: opt_string(obj, "comment"),
286        });
287    }
288
289    if types.is_empty() {
290        return Ok(None);
291    }
292    Ok(Some(ModelSnapshot {
293        migration_id,
294        entity_types: types,
295    }))
296}
297
298fn string_field(obj: &Map<String, Value>, key: &str) -> String {
299    obj.get(key)
300        .and_then(Value::as_str)
301        .unwrap_or_default()
302        .to_string()
303}
304
305fn opt_string(obj: &Map<String, Value>, key: &str) -> Option<String> {
306    obj.get(key).and_then(Value::as_str).map(str::to_string)
307}
308
309fn bool_field(obj: &Map<String, Value>, key: &str) -> bool {
310    obj.get(key).and_then(Value::as_bool).unwrap_or(false)
311}
312
313fn string_array(obj: &Map<String, Value>, key: &str) -> Vec<String> {
314    obj.get(key)
315        .and_then(Value::as_array)
316        .map(|items| {
317            items
318                .iter()
319                .filter_map(Value::as_str)
320                .map(str::to_string)
321                .collect()
322        })
323        .unwrap_or_default()
324}