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