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