Skip to main content

drizzle_cli/
snapshot.rs

1//! Snapshot conversion helpers reused from `drizzle-migrations`.
2
3pub use drizzle_migrations::parse_result_to_snapshot;
4
5#[cfg(test)]
6mod tests {
7    use super::*;
8    use drizzle_migrations::schema::Snapshot;
9    use drizzle_types::{Casing, Dialect};
10
11    /// Test that changing a column from Option<String> to String generates table recreation
12    #[test]
13    fn test_nullable_to_not_null_generates_migration() {
14        use drizzle_migrations::parser::SchemaParser;
15        use drizzle_migrations::sqlite::collection::SQLiteDDL;
16        use drizzle_migrations::sqlite::diff::compute_migration;
17
18        let prev_code = r#"
19#[SQLiteTable]
20pub struct User {
21    #[column(primary)]
22    pub id: i64,
23    pub name: String,
24    pub email: Option<String>,
25}
26"#;
27
28        let cur_code = r#"
29#[SQLiteTable]
30pub struct User {
31    #[column(primary)]
32    pub id: i64,
33    pub name: String,
34    pub email: String,
35}
36"#;
37
38        let prev_result = SchemaParser::parse(prev_code);
39        let cur_result = SchemaParser::parse(cur_code);
40
41        let prev_snapshot = parse_result_to_snapshot(&prev_result, Dialect::SQLite, None);
42        let cur_snapshot = parse_result_to_snapshot(&cur_result, Dialect::SQLite, None);
43
44        let (prev_ddl, cur_ddl) = match (&prev_snapshot, &cur_snapshot) {
45            (Snapshot::Sqlite(p), Snapshot::Sqlite(c)) => (
46                SQLiteDDL::from_entities(p.ddl.clone()),
47                SQLiteDDL::from_entities(c.ddl.clone()),
48            ),
49            _ => panic!("Expected SQLite snapshots"),
50        };
51
52        let prev_email = prev_ddl
53            .columns
54            .one("user", "email")
55            .expect("email column in prev");
56        let cur_email = cur_ddl
57            .columns
58            .one("user", "email")
59            .expect("email column in cur");
60        assert!(!prev_email.not_null, "Previous email should be nullable");
61        assert!(cur_email.not_null, "Current email should be NOT NULL");
62
63        let migration = compute_migration(&prev_ddl, &cur_ddl);
64
65        assert!(
66            !migration.sql_statements.is_empty(),
67            "Should generate migration SQL for nullable change"
68        );
69
70        assert_eq!(migration.sql_statements[0], "PRAGMA foreign_keys=OFF;");
71        assert!(
72            migration.sql_statements[1].starts_with("CREATE TABLE `__new_user`"),
73            "Expected CREATE TABLE `__new_user`, got: {}",
74            migration.sql_statements[1]
75        );
76        assert!(
77            migration.sql_statements[1].contains("`email` TEXT NOT NULL"),
78            "New table should have NOT NULL on email: {}",
79            migration.sql_statements[1]
80        );
81        assert_eq!(
82            migration.sql_statements[2],
83            "INSERT INTO `__new_user`(`id`, `name`, `email`) SELECT `id`, `name`, `email` FROM `user`;"
84        );
85        assert_eq!(migration.sql_statements[3], "DROP TABLE `user`;");
86        assert_eq!(
87            migration.sql_statements[4],
88            "ALTER TABLE `__new_user` RENAME TO `user`;"
89        );
90        assert_eq!(migration.sql_statements[5], "PRAGMA foreign_keys=ON;");
91    }
92
93    /// Test that changing a column from String to Option<String> generates table recreation
94    #[test]
95    fn test_not_null_to_nullable_generates_migration() {
96        use drizzle_migrations::parser::SchemaParser;
97        use drizzle_migrations::sqlite::collection::SQLiteDDL;
98        use drizzle_migrations::sqlite::diff::compute_migration;
99
100        let prev_code = r#"
101#[SQLiteTable]
102pub struct User {
103    #[column(primary)]
104    pub id: i64,
105    pub email: String,
106}
107"#;
108
109        let cur_code = r#"
110#[SQLiteTable]
111pub struct User {
112    #[column(primary)]
113    pub id: i64,
114    pub email: Option<String>,
115}
116"#;
117
118        let prev_result = SchemaParser::parse(prev_code);
119        let cur_result = SchemaParser::parse(cur_code);
120
121        let prev_snapshot = parse_result_to_snapshot(&prev_result, Dialect::SQLite, None);
122        let cur_snapshot = parse_result_to_snapshot(&cur_result, Dialect::SQLite, None);
123
124        let (prev_ddl, cur_ddl) = match (&prev_snapshot, &cur_snapshot) {
125            (Snapshot::Sqlite(p), Snapshot::Sqlite(c)) => (
126                SQLiteDDL::from_entities(p.ddl.clone()),
127                SQLiteDDL::from_entities(c.ddl.clone()),
128            ),
129            _ => panic!("Expected SQLite snapshots"),
130        };
131
132        let migration = compute_migration(&prev_ddl, &cur_ddl);
133
134        assert!(
135            !migration.sql_statements.is_empty(),
136            "Should generate migration SQL for nullable change"
137        );
138
139        assert_eq!(migration.sql_statements[0], "PRAGMA foreign_keys=OFF;");
140        assert!(
141            migration.sql_statements[1].starts_with("CREATE TABLE `__new_user`"),
142            "Expected CREATE TABLE `__new_user`, got: {}",
143            migration.sql_statements[1]
144        );
145        assert_eq!(migration.sql_statements[3], "DROP TABLE `user`;");
146        assert_eq!(
147            migration.sql_statements[4],
148            "ALTER TABLE `__new_user` RENAME TO `user`;"
149        );
150        assert_eq!(migration.sql_statements[5], "PRAGMA foreign_keys=ON;");
151    }
152
153    #[test]
154    fn test_postgres_schema_and_index_options_are_preserved() {
155        use drizzle_migrations::parser::SchemaParser;
156        use drizzle_migrations::postgres::ddl::PostgresEntity;
157
158        let code = r#"
159#[PostgresTable(schema = "auth")]
160pub struct Users {
161    #[column(primary)]
162    pub id: i32,
163}
164
165#[PostgresTable(schema = "app")]
166pub struct Sessions {
167    #[column(primary)]
168    pub id: i32,
169    #[column(references = Users::id)]
170    pub user_id: i32,
171}
172
173#[PostgresIndex(concurrent, method = "gin", where = "user_id > 0")]
174pub struct SessionsUserIdx(Sessions::user_id);
175"#;
176
177        let result = SchemaParser::parse(code);
178        let snapshot = parse_result_to_snapshot(&result, Dialect::PostgreSQL, None);
179
180        let snap = match snapshot {
181            Snapshot::Postgres(s) => s,
182            _ => panic!("Expected Postgres snapshot"),
183        };
184
185        let has_auth_schema = snap
186            .ddl
187            .iter()
188            .any(|e| matches!(e, PostgresEntity::Schema(s) if s.name.as_ref() == "auth"));
189        let has_app_schema = snap
190            .ddl
191            .iter()
192            .any(|e| matches!(e, PostgresEntity::Schema(s) if s.name.as_ref() == "app"));
193        assert!(has_auth_schema, "missing auth schema entity");
194        assert!(has_app_schema, "missing app schema entity");
195
196        let fk = snap.ddl.iter().find_map(|e| {
197            if let PostgresEntity::ForeignKey(fk) = e {
198                Some(fk)
199            } else {
200                None
201            }
202        });
203        let fk = fk.expect("expected foreign key");
204        assert_eq!(fk.schema.as_ref(), "app");
205        assert_eq!(fk.schema_to.as_ref(), "auth");
206
207        let idx = snap.ddl.iter().find_map(|e| {
208            if let PostgresEntity::Index(i) = e {
209                Some(i)
210            } else {
211                None
212            }
213        });
214        let idx = idx.expect("expected index");
215        assert!(idx.concurrently);
216        assert_eq!(idx.method.as_deref(), Some("gin"));
217        assert_eq!(idx.where_clause.as_deref(), Some("user_id > 0"));
218        assert_eq!(idx.schema.as_ref(), "app");
219    }
220
221    #[test]
222    fn test_sqlite_table_options_and_pk_name_are_preserved() {
223        use drizzle_migrations::parser::SchemaParser;
224        use drizzle_migrations::sqlite::SqliteEntity;
225
226        let code = r#"
227#[SQLiteTable(strict, without_rowid)]
228pub struct Accounts {
229    #[column(primary)]
230    pub id: i64,
231}
232"#;
233
234        let result = SchemaParser::parse(code);
235        let snapshot = parse_result_to_snapshot(&result, Dialect::SQLite, None);
236        let snap = match snapshot {
237            Snapshot::Sqlite(s) => s,
238            _ => panic!("Expected SQLite snapshot"),
239        };
240
241        let table = snap.ddl.iter().find_map(|e| {
242            if let SqliteEntity::Table(t) = e {
243                Some(t)
244            } else {
245                None
246            }
247        });
248        let table = table.expect("expected sqlite table");
249        assert!(table.strict, "strict should be preserved");
250        assert!(table.without_rowid, "without_rowid should be preserved");
251
252        let pk = snap.ddl.iter().find_map(|e| {
253            if let SqliteEntity::PrimaryKey(pk) = e {
254                Some(pk)
255            } else {
256                None
257            }
258        });
259        let pk = pk.expect("expected sqlite primary key");
260        assert_eq!(pk.name.as_ref(), "accounts_pkey");
261    }
262
263    #[test]
264    fn test_sqlite_casing_preserves_explicit_names() {
265        use drizzle_migrations::parser::SchemaParser;
266        use drizzle_migrations::sqlite::SqliteEntity;
267
268        let code = r#"
269#[SQLiteTable(name = "users_tbl")]
270pub struct UsersTable {
271    #[column(name = "user_id", primary)]
272    pub userId: i64,
273    pub emailAddress: String,
274}
275
276#[SQLiteIndex(name = "users_tbl_email_idx")]
277pub struct UsersEmailIdx(UsersTable::emailAddress);
278"#;
279
280        let result = SchemaParser::parse(code);
281        let snapshot = parse_result_to_snapshot(&result, Dialect::SQLite, Some(Casing::SnakeCase));
282        let snap = match snapshot {
283            Snapshot::Sqlite(s) => s,
284            _ => panic!("Expected SQLite snapshot"),
285        };
286
287        let table = snap.ddl.iter().find_map(|e| {
288            if let SqliteEntity::Table(t) = e {
289                Some(t)
290            } else {
291                None
292            }
293        });
294        let table = table.expect("expected sqlite table");
295        assert_eq!(table.name.as_ref(), "users_tbl");
296
297        let user_id = snap.ddl.iter().find_map(|e| {
298            if let SqliteEntity::Column(c) = e
299                && c.name.as_ref() == "user_id"
300            {
301                Some(c)
302            } else {
303                None
304            }
305        });
306        assert!(user_id.is_some(), "expected explicit column name user_id");
307
308        let email_col = snap.ddl.iter().find_map(|e| {
309            if let SqliteEntity::Column(c) = e
310                && c.name.as_ref() == "email_address"
311            {
312                Some(c)
313            } else {
314                None
315            }
316        });
317        assert!(
318            email_col.is_some(),
319            "expected inferred snake_case column name"
320        );
321
322        let index = snap.ddl.iter().find_map(|e| {
323            if let SqliteEntity::Index(i) = e {
324                Some(i)
325            } else {
326                None
327            }
328        });
329        let index = index.expect("expected sqlite index");
330        assert_eq!(index.name.as_ref(), "users_tbl_email_idx");
331    }
332
333    #[test]
334    fn test_postgres_casing_preserves_explicit_names() {
335        use drizzle_migrations::parser::SchemaParser;
336        use drizzle_migrations::postgres::ddl::PostgresEntity;
337
338        let code = r#"
339#[PostgresTable(schema = "auth", name = "users_tbl")]
340pub struct UsersTable {
341    #[column(name = "user_id", primary)]
342    pub userId: i32,
343    pub createdAt: String,
344}
345
346#[PostgresIndex(name = "users_tbl_created_idx")]
347pub struct UsersCreatedIdx(UsersTable::createdAt);
348"#;
349
350        let result = SchemaParser::parse(code);
351        let snapshot =
352            parse_result_to_snapshot(&result, Dialect::PostgreSQL, Some(Casing::SnakeCase));
353        let snap = match snapshot {
354            Snapshot::Postgres(s) => s,
355            _ => panic!("Expected Postgres snapshot"),
356        };
357
358        let table = snap.ddl.iter().find_map(|e| {
359            if let PostgresEntity::Table(t) = e {
360                Some(t)
361            } else {
362                None
363            }
364        });
365        let table = table.expect("expected postgres table");
366        assert_eq!(table.schema.as_ref(), "auth");
367        assert_eq!(table.name.as_ref(), "users_tbl");
368
369        let user_id = snap.ddl.iter().find_map(|e| {
370            if let PostgresEntity::Column(c) = e
371                && c.name.as_ref() == "user_id"
372            {
373                Some(c)
374            } else {
375                None
376            }
377        });
378        assert!(user_id.is_some(), "expected explicit column name user_id");
379
380        let created_at = snap.ddl.iter().find_map(|e| {
381            if let PostgresEntity::Column(c) = e
382                && c.name.as_ref() == "created_at"
383            {
384                Some(c)
385            } else {
386                None
387            }
388        });
389        assert!(
390            created_at.is_some(),
391            "expected inferred snake_case column name created_at"
392        );
393
394        let index = snap.ddl.iter().find_map(|e| {
395            if let PostgresEntity::Index(i) = e {
396                Some(i)
397            } else {
398                None
399            }
400        });
401        let index = index.expect("expected postgres index");
402        assert_eq!(index.name.as_ref(), "users_tbl_created_idx");
403        assert_eq!(index.schema.as_ref(), "auth");
404    }
405}