1pub 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]
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]
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}