1use std::path::Path;
7
8#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
9use crate::config::PostgresCreds;
10use crate::config::{Credentials, Dialect, Extension, IntrospectCasing};
11use crate::error::CliError;
12use crate::output;
13#[cfg(any(
14 test,
15 feature = "rusqlite",
16 feature = "libsql",
17 feature = "turso",
18 feature = "postgres-sync",
19 feature = "tokio-postgres",
20 feature = "d1-http",
21))]
22use drizzle_migrations::Migrations;
23use drizzle_migrations::schema::Snapshot;
24
25#[cfg(feature = "d1-http")]
26mod d1_http;
27mod filters;
28
29pub use filters::apply_snapshot_filters;
30#[cfg(test)]
31use filters::{compile_patterns, matches_patterns};
32
33#[derive(Debug)]
35pub struct MigrationResult {
36 pub applied_count: usize,
38 pub applied_migrations: Vec<String>,
40}
41
42#[derive(Debug, Clone)]
44pub struct MigrationPlan {
45 pub applied_count: usize,
47 pub pending_count: usize,
49 pub pending_migrations: Vec<String>,
51 pub pending_statements: usize,
53}
54
55#[cfg(any(
56 test,
57 feature = "rusqlite",
58 feature = "libsql",
59 feature = "turso",
60 feature = "postgres-sync",
61 feature = "tokio-postgres",
62 feature = "d1-http",
63))]
64#[derive(Debug, Clone)]
65pub(crate) struct AppliedMigrationRecord {
66 pub(crate) hash: String,
67 pub(crate) name: String,
68}
69
70#[derive(Debug, Clone)]
72pub struct PushPlan {
73 pub sql_statements: Vec<String>,
74 pub warnings: Vec<String>,
75 pub destructive: bool,
76}
77
78#[derive(Debug, Clone, Default)]
80pub struct SnapshotFilters {
81 pub tables: Option<Vec<String>>,
82 pub schemas: Option<Vec<String>>,
83 pub extensions: Option<Vec<Extension>>,
84}
85
86impl SnapshotFilters {
87 const fn is_empty(&self) -> bool {
88 self.tables.is_none() && self.schemas.is_none() && self.extensions.is_none()
89 }
90}
91
92pub fn plan_push(
99 credentials: &Credentials,
100 dialect: Dialect,
101 desired: &Snapshot,
102 breakpoints: bool,
103 filters: &SnapshotFilters,
104) -> Result<PushPlan, CliError> {
105 let mut current = introspect_database(credentials, dialect)?.snapshot;
106 apply_snapshot_filters(&mut current, dialect, filters)?;
107 let (sql_statements, warnings) = generate_push_sql(¤t, desired, breakpoints)?;
108 let destructive = sql_statements.iter().any(|s| is_destructive_statement(s));
109
110 Ok(PushPlan {
111 sql_statements,
112 warnings,
113 destructive,
114 })
115}
116
117pub fn apply_push(
125 credentials: &Credentials,
126 dialect: Dialect,
127 plan: &PushPlan,
128 force: bool,
129) -> Result<(), CliError> {
130 if plan.sql_statements.is_empty() {
131 return Ok(());
132 }
133
134 if plan.destructive && !force {
135 let confirmed = confirm_destructive()?;
136 if !confirmed {
137 return Ok(());
138 }
139 }
140
141 execute_statements(credentials, dialect, &plan.sql_statements)
142}
143
144#[allow(unused_variables)] pub fn plan_migrations(
156 credentials: &Credentials,
157 dialect: Dialect,
158 migrations_dir: &Path,
159 migrations_table: &str,
160 migrations_schema: &str,
161) -> Result<MigrationPlan, CliError> {
162 #[cfg(any(
163 feature = "rusqlite",
164 feature = "libsql",
165 feature = "turso",
166 feature = "postgres-sync",
167 feature = "tokio-postgres",
168 feature = "d1-http",
169 ))]
170 let set = load_migration_set(dialect, migrations_dir, migrations_table, migrations_schema)?;
171
172 match credentials {
173 #[cfg(feature = "rusqlite")]
174 Credentials::Sqlite { path } => inspect_sqlite_migrations(&set, path),
175
176 #[cfg(not(feature = "rusqlite"))]
177 Credentials::Sqlite { .. } => Err(CliError::MissingDriver {
178 dialect: "SQLite",
179 feature: "rusqlite",
180 }),
181
182 #[cfg(any(feature = "libsql", feature = "turso"))]
183 Credentials::Turso { url, auth_token } => {
184 if is_local_libsql(url) {
185 #[cfg(feature = "libsql")]
186 {
187 inspect_libsql_local_migrations(&set, url)
188 }
189 #[cfg(not(feature = "libsql"))]
190 {
191 Err(CliError::MissingDriver {
192 dialect: "LibSQL (local)",
193 feature: "libsql",
194 })
195 }
196 } else {
197 #[cfg(feature = "turso")]
198 {
199 inspect_turso_migrations(&set, url, auth_token.as_deref())
200 }
201 #[cfg(not(feature = "turso"))]
202 {
203 let _ = auth_token;
204 Err(CliError::MissingDriver {
205 dialect: "Turso (remote)",
206 feature: "turso",
207 })
208 }
209 }
210 }
211
212 #[cfg(all(not(feature = "turso"), not(feature = "libsql")))]
213 Credentials::Turso { .. } => Err(CliError::MissingDriver {
214 dialect: "Turso",
215 feature: "turso or libsql",
216 }),
217
218 Credentials::Postgres(creds) => {
219 let _ = creds;
220 core::cfg_select! {
221 feature = "postgres-sync" => inspect_postgres_sync_migrations(&set, creds),
222 feature = "tokio-postgres" => inspect_postgres_async_migrations(&set, creds),
223 _ => Err(CliError::MissingDriver {
224 dialect: "PostgreSQL",
225 feature: "postgres-sync or tokio-postgres",
226 }),
227 }
228 }
229
230 #[cfg(feature = "d1-http")]
231 Credentials::D1 {
232 account_id,
233 database_id,
234 token,
235 } => d1_http::inspect_migrations(&set, account_id, database_id, token),
236
237 #[cfg(not(feature = "d1-http"))]
238 Credentials::D1 { .. } => Err(CliError::MissingDriver {
239 dialect: "Cloudflare D1 (HTTP)",
240 feature: "d1-http",
241 }),
242
243 Credentials::AwsDataApi { .. } => Err(CliError::UnsupportedForDriver {
244 operation: "Migration planning against AWS Data API",
245 driver: "aws-data-api",
246 hint: "AWS RDS Data API schema ops are not yet wired into this CLI. For now, \
247 run the generated SQL with `aws rds-data execute-statement --sql=...` \
248 (or use tokio-postgres with a temporary direct connection).",
249 }),
250 }
251}
252
253pub fn verify_migrations(
261 credentials: &Credentials,
262 dialect: Dialect,
263 migrations_dir: &Path,
264 migrations_table: &str,
265 migrations_schema: &str,
266) -> Result<MigrationPlan, CliError> {
267 plan_migrations(
268 credentials,
269 dialect,
270 migrations_dir,
271 migrations_table,
272 migrations_schema,
273 )
274}
275
276#[allow(unused_variables)] pub fn run_migrations(
286 credentials: &Credentials,
287 dialect: Dialect,
288 migrations_dir: &Path,
289 migrations_table: &str,
290 migrations_schema: &str,
291) -> Result<MigrationResult, CliError> {
292 #[cfg(any(
293 feature = "rusqlite",
294 feature = "libsql",
295 feature = "turso",
296 feature = "postgres-sync",
297 feature = "tokio-postgres",
298 feature = "d1-http",
299 ))]
300 let set = load_migration_set(dialect, migrations_dir, migrations_table, migrations_schema)?;
301
302 match credentials {
303 #[cfg(feature = "rusqlite")]
304 Credentials::Sqlite { path } => run_sqlite_migrations(&set, path),
305
306 #[cfg(not(feature = "rusqlite"))]
307 Credentials::Sqlite { .. } => Err(CliError::MissingDriver {
308 dialect: "SQLite",
309 feature: "rusqlite",
310 }),
311
312 #[cfg(any(feature = "libsql", feature = "turso"))]
313 Credentials::Turso { url, auth_token } => {
314 if is_local_libsql(url) {
315 #[cfg(feature = "libsql")]
316 {
317 run_libsql_local_migrations(&set, url)
318 }
319 #[cfg(not(feature = "libsql"))]
320 {
321 Err(CliError::MissingDriver {
322 dialect: "LibSQL (local)",
323 feature: "libsql",
324 })
325 }
326 } else {
327 #[cfg(feature = "turso")]
328 {
329 run_turso_migrations(&set, url, auth_token.as_deref())
330 }
331 #[cfg(not(feature = "turso"))]
332 {
333 let _ = auth_token;
334 Err(CliError::MissingDriver {
335 dialect: "Turso (remote)",
336 feature: "turso",
337 })
338 }
339 }
340 }
341
342 #[cfg(all(not(feature = "turso"), not(feature = "libsql")))]
343 Credentials::Turso { .. } => Err(CliError::MissingDriver {
344 dialect: "Turso",
345 feature: "turso or libsql",
346 }),
347
348 Credentials::Postgres(creds) => {
350 let _ = creds;
351 core::cfg_select! {
352 feature = "postgres-sync" => run_postgres_sync_migrations(&set, creds),
353 feature = "tokio-postgres" => run_postgres_async_migrations(&set, creds),
354 _ => Err(CliError::MissingDriver {
355 dialect: "PostgreSQL",
356 feature: "postgres-sync or tokio-postgres",
357 }),
358 }
359 }
360
361 #[cfg(feature = "d1-http")]
362 Credentials::D1 {
363 account_id,
364 database_id,
365 token,
366 } => d1_http::run_migrations(&set, account_id, database_id, token),
367
368 #[cfg(not(feature = "d1-http"))]
369 Credentials::D1 { .. } => Err(CliError::MissingDriver {
370 dialect: "Cloudflare D1 (HTTP)",
371 feature: "d1-http",
372 }),
373
374 Credentials::AwsDataApi { .. } => Err(CliError::UnsupportedForDriver {
375 operation: "Running migrations against AWS Data API",
376 driver: "aws-data-api",
377 hint: "AWS RDS Data API migrations are not yet wired into this CLI. Run the \
378 generated SQL via `aws rds-data execute-statement` or connect directly \
379 with tokio-postgres.",
380 }),
381 }
382}
383
384#[cfg(any(
385 feature = "rusqlite",
386 feature = "libsql",
387 feature = "turso",
388 feature = "postgres-sync",
389 feature = "tokio-postgres",
390 feature = "d1-http",
391))]
392fn load_migration_set(
393 dialect: Dialect,
394 migrations_dir: &Path,
395 migrations_table: &str,
396 migrations_schema: &str,
397) -> Result<Migrations, CliError> {
398 let tracking = migration_tracking(dialect, migrations_table, migrations_schema);
399
400 let migrations = drizzle_migrations::MigrationDir::new(migrations_dir)
402 .discover()
403 .map_err(|e| CliError::Other(format!("Failed to load migrations: {e}")))?;
404 Ok(Migrations::with_tracking(
405 migrations,
406 dialect.to_base(),
407 tracking,
408 ))
409}
410
411#[cfg(any(
412 feature = "rusqlite",
413 feature = "libsql",
414 feature = "turso",
415 feature = "postgres-sync",
416 feature = "tokio-postgres",
417 feature = "d1-http",
418))]
419fn migration_tracking(
420 dialect: Dialect,
421 migrations_table: &str,
422 migrations_schema: &str,
423) -> drizzle_types::MigrationTracking {
424 let mut tracking = match dialect {
425 Dialect::Postgresql => drizzle_types::MigrationTracking::POSTGRES,
426 _ => drizzle_types::MigrationTracking::SQLITE,
427 };
428
429 if !migrations_table.trim().is_empty() {
430 tracking = tracking.table(migrations_table.to_owned());
431 }
432
433 if dialect == Dialect::Postgresql && !migrations_schema.trim().is_empty() {
434 tracking = tracking.schema(migrations_schema.to_owned());
435 }
436
437 tracking
438}
439
440#[cfg(any(
441 test,
442 feature = "rusqlite",
443 feature = "libsql",
444 feature = "turso",
445 feature = "postgres-sync",
446 feature = "tokio-postgres",
447 feature = "d1-http",
448))]
449pub(crate) fn build_migration_plan(
450 set: &Migrations,
451 applied: &[AppliedMigrationRecord],
452) -> Result<MigrationPlan, CliError> {
453 verify_applied_migrations_consistency(set, applied)?;
454
455 let applied_names = applied.iter().map(|m| m.name.clone()).collect::<Vec<_>>();
456 let pending = set.pending(&applied_names).collect::<Vec<_>>();
457
458 let pending_statements = pending
459 .iter()
460 .map(|m| {
461 m.statements()
462 .iter()
463 .filter(|stmt| !stmt.trim().is_empty())
464 .count()
465 })
466 .sum();
467
468 Ok(MigrationPlan {
469 applied_count: applied.len(),
470 pending_count: pending.len(),
471 pending_migrations: pending.iter().map(|m| m.tag().to_string()).collect(),
472 pending_statements,
473 })
474}
475
476#[cfg(any(
477 test,
478 feature = "rusqlite",
479 feature = "libsql",
480 feature = "turso",
481 feature = "postgres-sync",
482 feature = "tokio-postgres",
483 feature = "d1-http",
484))]
485fn verify_applied_migrations_consistency(
486 set: &Migrations,
487 applied: &[AppliedMigrationRecord],
488) -> Result<(), CliError> {
489 use std::collections::{HashMap, HashSet};
490
491 let mut local_by_name = HashMap::<&str, &str>::new();
492 for migration in set.all() {
493 if local_by_name
494 .insert(migration.name(), migration.hash())
495 .is_some()
496 {
497 return Err(CliError::MigrationError(format!(
498 "Local migrations contain duplicate name: {}",
499 migration.name()
500 )));
501 }
502 }
503
504 let mut seen_db_names = HashSet::<&str>::new();
505 for applied_row in applied {
506 if !seen_db_names.insert(applied_row.name.as_str()) {
507 return Err(CliError::MigrationError(format!(
508 "Database migration metadata contains duplicate name: {}",
509 applied_row.name
510 )));
511 }
512
513 let Some(local_hash) = local_by_name.get(applied_row.name.as_str()) else {
514 return Err(CliError::MigrationError(format!(
515 "Database contains applied migration not found locally (name: {})",
516 applied_row.name
517 )));
518 };
519
520 if *local_hash != applied_row.hash {
521 return Err(CliError::MigrationError(format!(
522 "Migration hash mismatch for {}: database={}, local={}",
523 applied_row.name, applied_row.hash, local_hash
524 )));
525 }
526 }
527
528 Ok(())
529}
530
531#[cfg(any(
532 feature = "rusqlite",
533 feature = "libsql",
534 feature = "turso",
535 feature = "postgres-sync",
536 feature = "tokio-postgres",
537))]
538fn escape_sql_literal(value: &str) -> String {
539 value.replace('\'', "''")
540}
541
542#[cfg(feature = "rusqlite")]
543fn ensure_sqlite_tracking_table(
544 conn: &rusqlite::Connection,
545 set: &Migrations,
546) -> Result<(), CliError> {
547 conn.execute(&set.create_table_sql(), [])
548 .map_err(|e| CliError::MigrationError(format!("Failed to create migrations table: {e}")))?;
549
550 let pragma_sql = format!(
551 "SELECT name FROM pragma_table_info('{}')",
552 escape_sql_literal(set.table_name())
553 );
554 let mut stmt = conn
555 .prepare(&pragma_sql)
556 .map_err(|e| CliError::MigrationError(e.to_string()))?;
557 let columns = stmt
558 .query_map([], |row| row.get::<_, String>(0))
559 .map_err(|e| CliError::MigrationError(e.to_string()))?
560 .collect::<Result<Vec<_>, _>>()
561 .map_err(|e| CliError::MigrationError(e.to_string()))?;
562 if columns.iter().any(|column| column == "name") {
563 return Ok(());
564 }
565
566 let mut stmt = conn
567 .prepare(&format!(
568 "SELECT id, hash, created_at FROM {} ORDER BY id ASC",
569 set.table_ident_sql()
570 ))
571 .map_err(|e| CliError::MigrationError(e.to_string()))?;
572 let applied = stmt
573 .query_map([], |row| {
574 Ok(drizzle_migrations::AppliedMigrationMetadata {
575 id: row.get::<_, Option<i64>>(0)?,
576 hash: row.get::<_, String>(1)?,
577 created_at: row.get::<_, i64>(2)?,
578 })
579 })
580 .map_err(|e| CliError::MigrationError(e.to_string()))?
581 .collect::<Result<Vec<_>, _>>()
582 .map_err(|e| CliError::MigrationError(e.to_string()))?;
583
584 let matched = drizzle_migrations::match_applied_migration_metadata(set.all(), &applied)
585 .map_err(|e| CliError::MigrationError(e.to_string()))?;
586
587 conn.execute("BEGIN", [])
588 .map_err(|e| CliError::MigrationError(e.to_string()))?;
589 let result = (|| -> Result<(), CliError> {
590 conn.execute(
591 &format!(
592 "ALTER TABLE {} ADD COLUMN \"name\" text",
593 set.table_ident_sql()
594 ),
595 [],
596 )
597 .map_err(|e| CliError::MigrationError(e.to_string()))?;
598 conn.execute(
599 &format!(
600 "ALTER TABLE {} ADD COLUMN \"applied_at\" TEXT",
601 set.table_ident_sql()
602 ),
603 [],
604 )
605 .map_err(|e| CliError::MigrationError(e.to_string()))?;
606
607 for row in matched {
608 let where_clause = if let Some(id) = row.id {
609 format!("\"id\" = {id}")
610 } else {
611 format!(
612 "\"created_at\" = {} AND \"hash\" = '{}'",
613 row.created_at,
614 escape_sql_literal(&row.hash)
615 )
616 };
617 conn.execute(
618 &format!(
619 "UPDATE {} SET \"name\" = '{}', \"applied_at\" = NULL WHERE {}",
620 set.table_ident_sql(),
621 escape_sql_literal(&row.name),
622 where_clause
623 ),
624 [],
625 )
626 .map_err(|e| CliError::MigrationError(e.to_string()))?;
627 }
628
629 Ok(())
630 })();
631
632 match result {
633 Ok(()) => {
634 conn.execute("COMMIT", [])
635 .map_err(|e| CliError::MigrationError(e.to_string()))?;
636 Ok(())
637 }
638 Err(err) => {
639 let _ = conn.execute("ROLLBACK", []);
640 Err(err)
641 }
642 }
643}
644
645#[cfg(any(feature = "libsql", feature = "turso"))]
646async fn ensure_sqlite_tracking_table_libsql(
647 conn: &libsql::Connection,
648 set: &Migrations,
649) -> Result<(), CliError> {
650 conn.execute(&set.create_table_sql(), ())
651 .await
652 .map_err(|e| CliError::MigrationError(format!("Failed to create migrations table: {e}")))?;
653
654 let pragma_sql = format!(
655 "SELECT name FROM pragma_table_info('{}')",
656 escape_sql_literal(set.table_name())
657 );
658 let mut rows = conn
659 .query(&pragma_sql, ())
660 .await
661 .map_err(|e| CliError::MigrationError(e.to_string()))?;
662 let mut has_name = false;
663 while let Some(row) = rows
664 .next()
665 .await
666 .map_err(|e| CliError::MigrationError(e.to_string()))?
667 {
668 let name = row
669 .get::<String>(0)
670 .map_err(|e| CliError::MigrationError(e.to_string()))?;
671 if name == "name" {
672 has_name = true;
673 break;
674 }
675 }
676 if has_name {
677 return Ok(());
678 }
679
680 let mut rows = conn
681 .query(
682 &format!(
683 "SELECT id, hash, created_at FROM {} ORDER BY id ASC",
684 set.table_ident_sql()
685 ),
686 (),
687 )
688 .await
689 .map_err(|e| CliError::MigrationError(e.to_string()))?;
690 let mut applied = Vec::new();
691 while let Some(row) = rows
692 .next()
693 .await
694 .map_err(|e| CliError::MigrationError(e.to_string()))?
695 {
696 let hash = row
697 .get::<String>(1)
698 .map_err(|e| CliError::MigrationError(e.to_string()))?;
699 let created_at = row
700 .get::<i64>(2)
701 .map_err(|e| CliError::MigrationError(e.to_string()))?;
702 applied.push(drizzle_migrations::AppliedMigrationMetadata {
703 id: row
704 .get::<Option<i64>>(0)
705 .map_err(|e| CliError::MigrationError(e.to_string()))?,
706 hash,
707 created_at,
708 });
709 }
710
711 let matched = drizzle_migrations::match_applied_migration_metadata(set.all(), &applied)
712 .map_err(|e| CliError::MigrationError(e.to_string()))?;
713
714 let tx = conn
715 .transaction()
716 .await
717 .map_err(|e| CliError::MigrationError(e.to_string()))?;
718 tx.execute(
719 &format!(
720 "ALTER TABLE {} ADD COLUMN \"name\" text",
721 set.table_ident_sql()
722 ),
723 (),
724 )
725 .await
726 .map_err(|e| CliError::MigrationError(e.to_string()))?;
727 tx.execute(
728 &format!(
729 "ALTER TABLE {} ADD COLUMN \"applied_at\" TEXT",
730 set.table_ident_sql()
731 ),
732 (),
733 )
734 .await
735 .map_err(|e| CliError::MigrationError(e.to_string()))?;
736
737 for row in matched {
738 let where_clause = if let Some(id) = row.id {
739 format!("\"id\" = {id}")
740 } else {
741 format!(
742 "\"created_at\" = {} AND \"hash\" = '{}'",
743 row.created_at,
744 escape_sql_literal(&row.hash)
745 )
746 };
747 tx.execute(
748 &format!(
749 "UPDATE {} SET \"name\" = '{}', \"applied_at\" = NULL WHERE {}",
750 set.table_ident_sql(),
751 escape_sql_literal(&row.name),
752 where_clause
753 ),
754 (),
755 )
756 .await
757 .map_err(|e| CliError::MigrationError(e.to_string()))?;
758 }
759
760 tx.commit()
761 .await
762 .map_err(|e| CliError::MigrationError(e.to_string()))?;
763 Ok(())
764}
765
766#[cfg(feature = "postgres-sync")]
767fn ensure_postgres_tracking_table_sync(
768 client: &mut postgres::Client,
769 set: &Migrations,
770) -> Result<(), CliError> {
771 client
772 .execute(&set.create_table_sql(), &[])
773 .map_err(|e| CliError::MigrationError(format!("Failed to create migrations table: {e}")))?;
774
775 let schema = set.schema_name().unwrap_or("public");
776 let rows = client
777 .query(
778 "SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2",
779 &[&schema, &set.table_name()],
780 )
781 .map_err(|e| CliError::MigrationError(e.to_string()))?;
782 let columns = rows
783 .iter()
784 .map(|row| row.try_get::<_, String>(0))
785 .collect::<Result<Vec<_>, postgres::Error>>()
786 .map_err(|e| CliError::MigrationError(e.to_string()))?;
787 if columns.iter().any(|column| column == "name") {
788 return Ok(());
789 }
790
791 let rows = client
792 .query(
793 &format!(
794 "SELECT id::bigint, hash, created_at FROM {} ORDER BY id ASC",
795 set.table_ident_sql()
796 ),
797 &[],
798 )
799 .map_err(|e| CliError::MigrationError(e.to_string()))?;
800 let applied = rows
801 .iter()
802 .map(|row| {
803 Ok(drizzle_migrations::AppliedMigrationMetadata {
804 id: row.try_get::<_, Option<i64>>(0)?,
805 hash: row.try_get::<_, String>(1)?,
806 created_at: row.try_get::<_, i64>(2)?,
807 })
808 })
809 .collect::<Result<Vec<_>, postgres::Error>>()
810 .map_err(|e| CliError::MigrationError(e.to_string()))?;
811
812 let matched = drizzle_migrations::match_applied_migration_metadata(set.all(), &applied)
813 .map_err(|e| CliError::MigrationError(e.to_string()))?;
814
815 client
816 .execute(
817 &format!(
818 "ALTER TABLE {} ADD COLUMN \"name\" TEXT",
819 set.table_ident_sql()
820 ),
821 &[],
822 )
823 .map_err(|e| CliError::MigrationError(e.to_string()))?;
824 client
825 .execute(
826 &format!(
827 "ALTER TABLE {} ADD COLUMN \"applied_at\" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP",
828 set.table_ident_sql()
829 ),
830 &[],
831 )
832 .map_err(|e| CliError::MigrationError(e.to_string()))?;
833
834 for row in matched {
835 let where_clause = if let Some(id) = row.id {
836 format!("\"id\" = {id}")
837 } else {
838 format!(
839 "\"created_at\" = {} AND \"hash\" = '{}'",
840 row.created_at,
841 escape_sql_literal(&row.hash)
842 )
843 };
844 client
845 .execute(
846 &format!(
847 "UPDATE {} SET \"name\" = '{}', \"applied_at\" = NULL WHERE {}",
848 set.table_ident_sql(),
849 escape_sql_literal(&row.name),
850 where_clause
851 ),
852 &[],
853 )
854 .map_err(|e| CliError::MigrationError(e.to_string()))?;
855 }
856
857 Ok(())
858}
859
860#[cfg(feature = "tokio-postgres")]
861async fn ensure_postgres_tracking_table_async(
862 client: &tokio_postgres::Client,
863 set: &Migrations,
864) -> Result<(), CliError> {
865 client
866 .execute(&set.create_table_sql(), &[])
867 .await
868 .map_err(|e| CliError::MigrationError(format!("Failed to create migrations table: {e}")))?;
869
870 let schema = set.schema_name().unwrap_or("public");
871 let rows = client
872 .query(
873 "SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2",
874 &[&schema, &set.table_name()],
875 )
876 .await
877 .map_err(|e| CliError::MigrationError(e.to_string()))?;
878 let columns = rows
879 .iter()
880 .map(|row| row.try_get::<_, String>(0))
881 .collect::<Result<Vec<_>, tokio_postgres::Error>>()
882 .map_err(|e| CliError::MigrationError(e.to_string()))?;
883 if columns.iter().any(|column| column == "name") {
884 return Ok(());
885 }
886
887 let rows = client
888 .query(
889 &format!(
890 "SELECT id::bigint, hash, created_at FROM {} ORDER BY id ASC",
891 set.table_ident_sql()
892 ),
893 &[],
894 )
895 .await
896 .map_err(|e| CliError::MigrationError(e.to_string()))?;
897 let applied = rows
898 .iter()
899 .map(|row| {
900 Ok(drizzle_migrations::AppliedMigrationMetadata {
901 id: row.try_get::<_, Option<i64>>(0)?,
902 hash: row.try_get::<_, String>(1)?,
903 created_at: row.try_get::<_, i64>(2)?,
904 })
905 })
906 .collect::<Result<Vec<_>, tokio_postgres::Error>>()
907 .map_err(|e| CliError::MigrationError(e.to_string()))?;
908
909 let matched = drizzle_migrations::match_applied_migration_metadata(set.all(), &applied)
910 .map_err(|e| CliError::MigrationError(e.to_string()))?;
911
912 client
913 .execute(
914 &format!(
915 "ALTER TABLE {} ADD COLUMN \"name\" TEXT",
916 set.table_ident_sql()
917 ),
918 &[],
919 )
920 .await
921 .map_err(|e| CliError::MigrationError(e.to_string()))?;
922 client
923 .execute(
924 &format!(
925 "ALTER TABLE {} ADD COLUMN \"applied_at\" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP",
926 set.table_ident_sql()
927 ),
928 &[],
929 )
930 .await
931 .map_err(|e| CliError::MigrationError(e.to_string()))?;
932
933 for row in matched {
934 let where_clause = if let Some(id) = row.id {
935 format!("\"id\" = {id}")
936 } else {
937 format!(
938 "\"created_at\" = {} AND \"hash\" = '{}'",
939 row.created_at,
940 escape_sql_literal(&row.hash)
941 )
942 };
943 client
944 .execute(
945 &format!(
946 "UPDATE {} SET \"name\" = '{}', \"applied_at\" = NULL WHERE {}",
947 set.table_ident_sql(),
948 escape_sql_literal(&row.name),
949 where_clause
950 ),
951 &[],
952 )
953 .await
954 .map_err(|e| CliError::MigrationError(e.to_string()))?;
955 }
956
957 Ok(())
958}
959
960fn is_destructive_statement(sql: &str) -> bool {
961 let s = sql.trim().to_uppercase();
962 s.contains("DROP TABLE")
963 || s.contains("DROP COLUMN")
964 || s.contains("DROP INDEX")
965 || s.contains("DROP VIEW")
966 || s.contains("DROP MATERIALIZED VIEW")
967 || s.contains("DROP TYPE")
968 || s.contains("DROP SCHEMA")
969 || s.contains("DROP SEQUENCE")
970 || s.contains("DROP ROLE")
971 || s.contains("DROP POLICY")
972 || s.contains("TRUNCATE")
973 || (s.contains("ALTER TABLE") && s.contains(" DROP "))
974}
975
976#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
977fn is_postgres_concurrent_index_statement(sql: &str) -> bool {
978 drizzle_migrations::is_postgres_concurrent_index_statement(sql)
979}
980
981#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
982fn has_postgres_concurrent_index(statements: &[String]) -> bool {
983 statements
984 .iter()
985 .any(|stmt| is_postgres_concurrent_index_statement(stmt))
986}
987
988fn confirm_destructive() -> Result<bool, CliError> {
989 use std::io::{self, IsTerminal, Write};
990
991 if !io::stdin().is_terminal() {
992 return Err(CliError::Other(
993 "Refusing to apply potentially destructive changes in non-interactive mode. Use --explain or --force."
994 .into(),
995 ));
996 }
997
998 println!(
999 "{}",
1000 output::warning("Potentially destructive changes detected (DROP/TRUNCATE/etc).")
1001 );
1002 print!("Apply anyway? [y/N]: ");
1003 io::stdout()
1004 .flush()
1005 .map_err(|e| CliError::IoError(e.to_string()))?;
1006
1007 let mut line = String::new();
1008 io::stdin()
1009 .read_line(&mut line)
1010 .map_err(|e| CliError::IoError(e.to_string()))?;
1011 let ans = line.trim().to_ascii_lowercase();
1012 Ok(ans == "y" || ans == "yes")
1013}
1014
1015fn generate_push_sql(
1016 current: &Snapshot,
1017 desired: &Snapshot,
1018 breakpoints: bool,
1019) -> Result<(Vec<String>, Vec<String>), CliError> {
1020 match (current, desired) {
1021 (Snapshot::Sqlite(prev_snap), Snapshot::Sqlite(curr_snap)) => {
1022 use drizzle_migrations::sqlite::collection::SQLiteDDL;
1023 use drizzle_migrations::sqlite::diff::compute_migration;
1024
1025 let prev_ddl = SQLiteDDL::from_entities(prev_snap.ddl.clone());
1026 let cur_ddl = SQLiteDDL::from_entities(curr_snap.ddl.clone());
1027
1028 let diff = compute_migration(&prev_ddl, &cur_ddl);
1029 Ok((diff.sql_statements, diff.warnings))
1030 }
1031 (Snapshot::Postgres(prev_snap), Snapshot::Postgres(curr_snap)) => {
1032 use drizzle_migrations::postgres::diff_full_snapshots;
1033 use drizzle_migrations::postgres::statements::PostgresGenerator;
1034
1035 let diff = diff_full_snapshots(prev_snap, curr_snap);
1036 let generator = PostgresGenerator::new().with_breakpoints(breakpoints);
1037 Ok((generator.generate(&diff.diffs), Vec::new()))
1038 }
1039 _ => Err(CliError::DialectMismatch),
1040 }
1041}
1042
1043fn execute_statements(
1044 credentials: &Credentials,
1045 _dialect: Dialect,
1046 statements: &[String],
1047) -> Result<(), CliError> {
1048 let _ = statements;
1051
1052 match credentials {
1053 #[cfg(feature = "rusqlite")]
1054 Credentials::Sqlite { path } => execute_sqlite_statements(path, statements),
1055
1056 #[cfg(not(feature = "rusqlite"))]
1057 Credentials::Sqlite { .. } => Err(CliError::MissingDriver {
1058 dialect: "SQLite",
1059 feature: "rusqlite",
1060 }),
1061
1062 #[cfg(any(feature = "libsql", feature = "turso"))]
1063 Credentials::Turso { url, auth_token } => {
1064 if is_local_libsql(url) {
1065 #[cfg(feature = "libsql")]
1066 {
1067 execute_libsql_local_statements(url, statements)
1068 }
1069 #[cfg(not(feature = "libsql"))]
1070 {
1071 Err(CliError::MissingDriver {
1072 dialect: "LibSQL (local)",
1073 feature: "libsql",
1074 })
1075 }
1076 } else {
1077 #[cfg(feature = "turso")]
1078 {
1079 execute_turso_statements(url, auth_token.as_deref(), statements)
1080 }
1081 #[cfg(not(feature = "turso"))]
1082 {
1083 let _ = auth_token;
1084 Err(CliError::MissingDriver {
1085 dialect: "Turso (remote)",
1086 feature: "turso",
1087 })
1088 }
1089 }
1090 }
1091
1092 #[cfg(all(not(feature = "turso"), not(feature = "libsql")))]
1093 Credentials::Turso { .. } => Err(CliError::MissingDriver {
1094 dialect: "Turso",
1095 feature: "turso or libsql",
1096 }),
1097
1098 Credentials::Postgres(creds) => {
1099 let _ = (creds, &statements);
1100 core::cfg_select! {
1101 feature = "postgres-sync" => execute_postgres_sync_statements(creds, statements),
1102 feature = "tokio-postgres" => execute_postgres_async_statements(creds, statements),
1103 _ => Err(CliError::MissingDriver {
1104 dialect: "PostgreSQL",
1105 feature: "postgres-sync or tokio-postgres",
1106 }),
1107 }
1108 }
1109
1110 #[cfg(feature = "d1-http")]
1111 Credentials::D1 {
1112 account_id,
1113 database_id,
1114 token,
1115 } => d1_http::execute_statements(account_id, database_id, token, statements),
1116
1117 #[cfg(not(feature = "d1-http"))]
1118 Credentials::D1 { .. } => Err(CliError::MissingDriver {
1119 dialect: "Cloudflare D1 (HTTP)",
1120 feature: "d1-http",
1121 }),
1122
1123 Credentials::AwsDataApi { .. } => Err(CliError::UnsupportedForDriver {
1124 operation: "Direct SQL execution against AWS Data API",
1125 driver: "aws-data-api",
1126 hint: "Use `aws rds-data execute-statement --sql=\"...\"` with the matching \
1127 --resource-arn / --secret-arn / --database, or connect directly via \
1128 tokio-postgres.",
1129 }),
1130 }
1131}
1132
1133#[allow(dead_code)]
1135fn is_local_libsql(url: &str) -> bool {
1136 url.starts_with("file:")
1137 || url.starts_with("./")
1138 || url.starts_with('/')
1139 || !url.contains("://")
1140}
1141
1142#[cfg(feature = "rusqlite")]
1147fn execute_sqlite_statements(path: &str, statements: &[String]) -> Result<(), CliError> {
1148 let conn = rusqlite::Connection::open(path).map_err(|e| {
1149 CliError::ConnectionError(format!("Failed to open SQLite database '{path}': {e}"))
1150 })?;
1151
1152 conn.execute("BEGIN", [])
1153 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1154
1155 for stmt in statements {
1156 let s = stmt.trim();
1157 if s.is_empty() {
1158 continue;
1159 }
1160 if let Err(e) = conn.execute(s, []) {
1161 let _ = conn.execute("ROLLBACK", []);
1162 return Err(CliError::MigrationError(format!(
1163 "Statement failed: {e}\n{s}"
1164 )));
1165 }
1166 }
1167
1168 conn.execute("COMMIT", [])
1169 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1170
1171 Ok(())
1172}
1173
1174#[cfg(feature = "rusqlite")]
1175fn run_sqlite_migrations(set: &Migrations, path: &str) -> Result<MigrationResult, CliError> {
1176 let conn = rusqlite::Connection::open(path).map_err(|e| {
1177 CliError::ConnectionError(format!("Failed to open SQLite database '{path}': {e}"))
1178 })?;
1179
1180 ensure_sqlite_tracking_table(&conn, set)?;
1181 conn.busy_timeout(std::time::Duration::from_secs(30))
1182 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1183 conn.execute("BEGIN IMMEDIATE", [])
1184 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1185 let applied_names = query_applied_names_sqlite(&conn, set)?;
1186
1187 let pending: Vec<_> = set.pending(&applied_names).collect();
1189 if pending.is_empty() {
1190 conn.execute("COMMIT", [])
1191 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1192 return Ok(MigrationResult {
1193 applied_count: 0,
1194 applied_migrations: vec![],
1195 });
1196 }
1197
1198 let mut applied = Vec::new();
1199 for migration in &pending {
1200 for stmt in migration.statements() {
1201 if !stmt.trim().is_empty()
1202 && let Err(e) = conn.execute(stmt, [])
1203 {
1204 let _ = conn.execute("ROLLBACK", []);
1205 return Err(CliError::MigrationError(format!(
1206 "Migration '{}' failed: {}",
1207 migration.hash(),
1208 e
1209 )));
1210 }
1211 }
1212 if let Err(e) = conn.execute(&set.record_migration_sql(migration), []) {
1213 let _ = conn.execute("ROLLBACK", []);
1214 return Err(CliError::MigrationError(e.to_string()));
1215 }
1216 applied.push(migration.hash().to_string());
1217 }
1218
1219 conn.execute("COMMIT", [])
1220 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1221
1222 Ok(MigrationResult {
1223 applied_count: applied.len(),
1224 applied_migrations: applied,
1225 })
1226}
1227
1228#[cfg(feature = "rusqlite")]
1229fn inspect_sqlite_migrations(set: &Migrations, path: &str) -> Result<MigrationPlan, CliError> {
1230 let conn = rusqlite::Connection::open(path).map_err(|e| {
1231 CliError::ConnectionError(format!("Failed to open SQLite database '{path}': {e}"))
1232 })?;
1233
1234 ensure_sqlite_tracking_table(&conn, set)?;
1235 let applied = query_applied_records_sqlite(&conn, set)?;
1236 build_migration_plan(set, &applied)
1237}
1238
1239#[cfg(feature = "rusqlite")]
1240fn is_sqlite_missing_table(error: &rusqlite::Error) -> bool {
1241 matches!(
1242 error,
1243 rusqlite::Error::SqliteFailure(_, Some(message)) if message.contains("no such table")
1244 )
1245}
1246
1247#[cfg(feature = "rusqlite")]
1248fn query_applied_records_sqlite(
1249 conn: &rusqlite::Connection,
1250 set: &Migrations,
1251) -> Result<Vec<AppliedMigrationRecord>, CliError> {
1252 let sql = format!(
1253 r#"SELECT hash, "name" FROM {} WHERE "name" IS NOT NULL ORDER BY id;"#,
1254 set.table_ident_sql()
1255 );
1256 let mut stmt = match conn.prepare(&sql) {
1257 Ok(stmt) => stmt,
1258 Err(error) if is_sqlite_missing_table(&error) => return Ok(vec![]),
1259 Err(error) => return Err(CliError::MigrationError(error.to_string())),
1260 };
1261
1262 let mut applied = Vec::new();
1263 let rows = stmt
1264 .query_map([], |row| {
1265 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
1266 })
1267 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1268
1269 for row in rows {
1270 let (hash, name) = row.map_err(|e| CliError::MigrationError(e.to_string()))?;
1271 applied.push(AppliedMigrationRecord { hash, name });
1272 }
1273
1274 Ok(applied)
1275}
1276
1277#[cfg(feature = "rusqlite")]
1278fn query_applied_names_sqlite(
1279 conn: &rusqlite::Connection,
1280 set: &Migrations,
1281) -> Result<Vec<String>, CliError> {
1282 let mut stmt = match conn.prepare(&set.applied_names_sql()) {
1283 Ok(stmt) => stmt,
1284 Err(error) if is_sqlite_missing_table(&error) => return Ok(vec![]),
1285 Err(error) => return Err(CliError::MigrationError(error.to_string())),
1286 };
1287
1288 let names = stmt
1289 .query_map([], |row| row.get::<_, String>(0))
1290 .map_err(|e| CliError::MigrationError(e.to_string()))?
1291 .collect::<Result<Vec<_>, _>>()
1292 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1293
1294 Ok(names)
1295}
1296
1297#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
1302fn postgres_tls_connector(
1303 mode: crate::config::PostgresSslMode,
1304) -> Result<postgres_native_tls::MakeTlsConnector, CliError> {
1305 let mut builder = native_tls::TlsConnector::builder();
1306 if mode == crate::config::PostgresSslMode::VerifyCa {
1307 builder.danger_accept_invalid_hostnames(true);
1308 }
1309 let connector = builder
1310 .build()
1311 .map_err(|error| CliError::ConnectionError(error.to_string()))?;
1312 Ok(postgres_native_tls::MakeTlsConnector::new(connector))
1313}
1314
1315#[cfg(feature = "postgres-sync")]
1316fn connect_postgres_sync(creds: &PostgresCreds) -> Result<postgres::Client, CliError> {
1317 let connection = creds
1318 .connection_config()
1319 .map_err(CliError::ConnectionError)?;
1320 let config = match creds {
1321 PostgresCreds::Url(url) => url
1322 .parse::<postgres::Config>()
1323 .map_err(|error| CliError::ConnectionError(error.to_string()))?,
1324 PostgresCreds::Host {
1325 host,
1326 port,
1327 user,
1328 password,
1329 database,
1330 ..
1331 } => {
1332 let mut config = postgres::Config::new();
1333 config
1334 .host(host.as_ref())
1335 .port(*port)
1336 .dbname(database.as_ref());
1337 if let Some(user) = user {
1338 config.user(user.as_ref());
1339 }
1340 if let Some(password) = password {
1341 config.password(password.as_bytes());
1342 }
1343 config.ssl_mode(connection.config.get_ssl_mode());
1344 config
1345 }
1346 };
1347 if connection.ssl == crate::config::PostgresSslMode::Disable {
1348 config
1349 .connect(postgres::NoTls)
1350 .map_err(|error| CliError::ConnectionError(error.to_string()))
1351 } else {
1352 let connector = postgres_tls_connector(connection.ssl)?;
1353 config
1354 .connect(connector)
1355 .map_err(|error| CliError::ConnectionError(error.to_string()))
1356 }
1357}
1358
1359#[cfg(feature = "tokio-postgres")]
1360async fn connect_postgres_async(creds: &PostgresCreds) -> Result<tokio_postgres::Client, CliError> {
1361 let connection = creds
1362 .connection_config()
1363 .map_err(CliError::ConnectionError)?;
1364 if connection.ssl == crate::config::PostgresSslMode::Disable {
1365 let (client, driver) = connection
1366 .config
1367 .connect(tokio_postgres::NoTls)
1368 .await
1369 .map_err(|error| CliError::ConnectionError(error.to_string()))?;
1370 tokio::spawn(async move {
1371 if let Err(error) = driver.await {
1372 eprintln!("PostgreSQL connection error: {error}");
1373 }
1374 });
1375 Ok(client)
1376 } else {
1377 let connector = postgres_tls_connector(connection.ssl)?;
1378 let (client, driver) = connection
1379 .config
1380 .connect(connector)
1381 .await
1382 .map_err(|error| CliError::ConnectionError(error.to_string()))?;
1383 tokio::spawn(async move {
1384 if let Err(error) = driver.await {
1385 eprintln!("PostgreSQL connection error: {error}");
1386 }
1387 });
1388 Ok(client)
1389 }
1390}
1391
1392#[cfg(feature = "postgres-sync")]
1393fn execute_postgres_sync_statements(
1394 creds: &PostgresCreds,
1395 statements: &[String],
1396) -> Result<(), CliError> {
1397 let mut client = connect_postgres_sync(creds)?;
1398
1399 if has_postgres_concurrent_index(statements) {
1400 for stmt in statements {
1402 let s = stmt.trim();
1403 if s.is_empty() {
1404 continue;
1405 }
1406 client
1407 .execute(s, &[])
1408 .map_err(|e| CliError::MigrationError(format!("Statement failed: {e}\n{s}")))?;
1409 }
1410 } else {
1411 let mut tx = client
1412 .transaction()
1413 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1414
1415 for stmt in statements {
1416 let s = stmt.trim();
1417 if s.is_empty() {
1418 continue;
1419 }
1420 tx.execute(s, &[])
1421 .map_err(|e| CliError::MigrationError(format!("Statement failed: {e}\n{s}")))?;
1422 }
1423
1424 tx.commit()
1425 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1426 }
1427
1428 Ok(())
1429}
1430
1431#[cfg(feature = "postgres-sync")]
1432fn run_postgres_sync_migrations(
1433 set: &Migrations,
1434 creds: &PostgresCreds,
1435) -> Result<MigrationResult, CliError> {
1436 let mut client = connect_postgres_sync(creds)?;
1437
1438 if let Some(schema_sql) = set.create_schema_sql() {
1440 client
1441 .execute(&schema_sql, &[])
1442 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1443 }
1444
1445 let lock_key = set.postgres_advisory_lock_key();
1446 client
1447 .query_one("SELECT pg_advisory_lock($1)", &[&lock_key])
1448 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1449 let result = run_postgres_sync_migrations_locked(&mut client, set);
1450 let unlock = client
1451 .query_one("SELECT pg_advisory_unlock($1)", &[&lock_key])
1452 .map_err(|error| CliError::MigrationError(error.to_string()));
1453 match (result, unlock) {
1454 (Ok(result), Ok(_)) => Ok(result),
1455 (Err(error), _) => Err(error),
1456 (Ok(_), Err(error)) => Err(error),
1457 }
1458}
1459
1460#[cfg(feature = "postgres-sync")]
1461fn run_postgres_sync_migrations_locked(
1462 client: &mut postgres::Client,
1463 set: &Migrations,
1464) -> Result<MigrationResult, CliError> {
1465 ensure_postgres_tracking_table_sync(client, set)?;
1466 let rows = client
1467 .query(&set.applied_names_sql(), &[])
1468 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1469 let applied_names = rows
1470 .iter()
1471 .map(|row| row.try_get(0))
1472 .collect::<Result<Vec<String>, _>>()
1473 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1474 let pending: Vec<_> = set.pending(&applied_names).collect();
1475 if pending.is_empty() {
1476 return Ok(MigrationResult {
1477 applied_count: 0,
1478 applied_migrations: vec![],
1479 });
1480 }
1481
1482 let mut applied = Vec::new();
1483 if set.has_postgres_concurrent_index() {
1484 for migration in &pending {
1485 for statement in migration.statements() {
1486 if !statement.trim().is_empty() {
1487 client.execute(statement, &[]).map_err(|error| {
1488 CliError::MigrationError(format!(
1489 "Migration '{}' failed: {error}",
1490 migration.hash()
1491 ))
1492 })?;
1493 }
1494 }
1495 client
1496 .execute(&set.record_migration_sql(migration), &[])
1497 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1498 applied.push(migration.hash().to_string());
1499 }
1500 } else {
1501 let mut transaction = client
1502 .transaction()
1503 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1504 for migration in &pending {
1505 for statement in migration.statements() {
1506 if !statement.trim().is_empty() {
1507 transaction.execute(statement, &[]).map_err(|error| {
1508 CliError::MigrationError(format!(
1509 "Migration '{}' failed: {error}",
1510 migration.hash()
1511 ))
1512 })?;
1513 }
1514 }
1515 transaction
1516 .execute(&set.record_migration_sql(migration), &[])
1517 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1518 applied.push(migration.hash().to_string());
1519 }
1520 transaction
1521 .commit()
1522 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1523 }
1524
1525 Ok(MigrationResult {
1526 applied_count: applied.len(),
1527 applied_migrations: applied,
1528 })
1529}
1530
1531#[cfg(feature = "postgres-sync")]
1532fn inspect_postgres_sync_migrations(
1533 set: &Migrations,
1534 creds: &PostgresCreds,
1535) -> Result<MigrationPlan, CliError> {
1536 let mut client = connect_postgres_sync(creds)?;
1537
1538 if let Some(schema_sql) = set.create_schema_sql() {
1539 client
1540 .execute(&schema_sql, &[])
1541 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1542 }
1543
1544 ensure_postgres_tracking_table_sync(&mut client, set)?;
1545 let applied = query_applied_records_postgres_sync(&mut client, set)?;
1546 build_migration_plan(set, &applied)
1547}
1548
1549#[cfg(feature = "postgres-sync")]
1550fn query_applied_records_postgres_sync(
1551 client: &mut postgres::Client,
1552 set: &Migrations,
1553) -> Result<Vec<AppliedMigrationRecord>, CliError> {
1554 let sql = format!(
1555 r#"SELECT hash, "name" FROM {} WHERE "name" IS NOT NULL ORDER BY id;"#,
1556 set.table_ident_sql()
1557 );
1558 let rows = client
1559 .query(&sql, &[])
1560 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1561
1562 let mut applied = Vec::new();
1563 for row in rows {
1564 let hash = row
1565 .try_get::<_, Option<String>>(0)
1566 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1567 let name = row
1568 .try_get::<_, Option<String>>(1)
1569 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1570 if let (Some(hash), Some(name)) = (hash, name) {
1571 applied.push(AppliedMigrationRecord { hash, name });
1572 }
1573 }
1574
1575 Ok(applied)
1576}
1577
1578#[cfg(all(feature = "tokio-postgres", not(feature = "postgres-sync")))]
1583fn execute_postgres_async_statements(
1584 creds: &PostgresCreds,
1585 statements: &[String],
1586) -> Result<(), CliError> {
1587 let rt = tokio::runtime::Builder::new_current_thread()
1588 .enable_all()
1589 .build()
1590 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {}", e)))?;
1591
1592 rt.block_on(execute_postgres_async_inner(creds, statements))
1593}
1594
1595#[cfg(all(feature = "tokio-postgres", not(feature = "postgres-sync")))]
1596async fn execute_postgres_async_inner(
1597 creds: &PostgresCreds,
1598 statements: &[String],
1599) -> Result<(), CliError> {
1600 let mut client = connect_postgres_async(creds).await?;
1601
1602 if has_postgres_concurrent_index(statements) {
1603 for stmt in statements {
1605 let s = stmt.trim();
1606 if s.is_empty() {
1607 continue;
1608 }
1609 client
1610 .execute(s, &[])
1611 .await
1612 .map_err(|e| CliError::MigrationError(format!("Statement failed: {}\n{}", e, s)))?;
1613 }
1614 } else {
1615 let tx = client
1616 .transaction()
1617 .await
1618 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1619
1620 for stmt in statements {
1621 let s = stmt.trim();
1622 if s.is_empty() {
1623 continue;
1624 }
1625 tx.execute(s, &[])
1626 .await
1627 .map_err(|e| CliError::MigrationError(format!("Statement failed: {}\n{}", e, s)))?;
1628 }
1629
1630 tx.commit()
1631 .await
1632 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1633 }
1634
1635 Ok(())
1636}
1637
1638#[cfg(feature = "tokio-postgres")]
1639#[allow(dead_code)] fn run_postgres_async_migrations(
1641 set: &Migrations,
1642 creds: &PostgresCreds,
1643) -> Result<MigrationResult, CliError> {
1644 let rt = tokio::runtime::Builder::new_current_thread()
1645 .enable_all()
1646 .build()
1647 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
1648
1649 rt.block_on(run_postgres_async_inner(set, creds))
1650}
1651
1652#[cfg(feature = "tokio-postgres")]
1653#[allow(dead_code)] fn inspect_postgres_async_migrations(
1655 set: &Migrations,
1656 creds: &PostgresCreds,
1657) -> Result<MigrationPlan, CliError> {
1658 let rt = tokio::runtime::Builder::new_current_thread()
1659 .enable_all()
1660 .build()
1661 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
1662
1663 rt.block_on(inspect_postgres_async_inner(set, creds))
1664}
1665
1666#[cfg(feature = "tokio-postgres")]
1667#[allow(dead_code)]
1668async fn inspect_postgres_async_inner(
1669 set: &Migrations,
1670 creds: &PostgresCreds,
1671) -> Result<MigrationPlan, CliError> {
1672 let client = connect_postgres_async(creds).await?;
1673
1674 if let Some(schema_sql) = set.create_schema_sql() {
1675 client
1676 .execute(&schema_sql, &[])
1677 .await
1678 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1679 }
1680
1681 ensure_postgres_tracking_table_async(&client, set).await?;
1682 let applied = query_applied_records_postgres_async(&client, set).await?;
1683 build_migration_plan(set, &applied)
1684}
1685
1686#[cfg(feature = "tokio-postgres")]
1687async fn query_applied_records_postgres_async(
1688 client: &tokio_postgres::Client,
1689 set: &Migrations,
1690) -> Result<Vec<AppliedMigrationRecord>, CliError> {
1691 let sql = format!(
1692 r#"SELECT hash, "name" FROM {} WHERE "name" IS NOT NULL ORDER BY id;"#,
1693 set.table_ident_sql()
1694 );
1695 let rows = client
1696 .query(&sql, &[])
1697 .await
1698 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1699
1700 let mut applied = Vec::new();
1701 for row in rows {
1702 let hash = row
1703 .try_get::<_, Option<String>>(0)
1704 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1705 let name = row
1706 .try_get::<_, Option<String>>(1)
1707 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1708 if let (Some(hash), Some(name)) = (hash, name) {
1709 applied.push(AppliedMigrationRecord { hash, name });
1710 }
1711 }
1712
1713 Ok(applied)
1714}
1715
1716#[cfg(feature = "tokio-postgres")]
1717#[allow(dead_code)]
1718async fn run_postgres_async_inner(
1719 set: &Migrations,
1720 creds: &PostgresCreds,
1721) -> Result<MigrationResult, CliError> {
1722 let mut client = connect_postgres_async(creds).await?;
1723
1724 if let Some(schema_sql) = set.create_schema_sql() {
1726 client
1727 .execute(&schema_sql, &[])
1728 .await
1729 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1730 }
1731
1732 let lock_key = set.postgres_advisory_lock_key();
1733 client
1734 .query_one("SELECT pg_advisory_lock($1)", &[&lock_key])
1735 .await
1736 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1737 let result = run_postgres_async_migrations_locked(&mut client, set).await;
1738 let unlock = client
1739 .query_one("SELECT pg_advisory_unlock($1)", &[&lock_key])
1740 .await
1741 .map_err(|error| CliError::MigrationError(error.to_string()));
1742 match (result, unlock) {
1743 (Ok(result), Ok(_)) => Ok(result),
1744 (Err(error), _) => Err(error),
1745 (Ok(_), Err(error)) => Err(error),
1746 }
1747}
1748
1749#[cfg(feature = "tokio-postgres")]
1750async fn run_postgres_async_migrations_locked(
1751 client: &mut tokio_postgres::Client,
1752 set: &Migrations,
1753) -> Result<MigrationResult, CliError> {
1754 ensure_postgres_tracking_table_async(client, set).await?;
1755 let rows = client
1756 .query(&set.applied_names_sql(), &[])
1757 .await
1758 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1759 let applied_names = rows
1760 .iter()
1761 .map(|row| row.try_get(0))
1762 .collect::<Result<Vec<String>, _>>()
1763 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1764 let pending: Vec<_> = set.pending(&applied_names).collect();
1765 if pending.is_empty() {
1766 return Ok(MigrationResult {
1767 applied_count: 0,
1768 applied_migrations: vec![],
1769 });
1770 }
1771
1772 let mut applied = Vec::new();
1773 if set.has_postgres_concurrent_index() {
1774 for migration in &pending {
1775 for statement in migration.statements() {
1776 if !statement.trim().is_empty() {
1777 client.execute(statement, &[]).await.map_err(|error| {
1778 CliError::MigrationError(format!(
1779 "Migration '{}' failed: {error}",
1780 migration.hash()
1781 ))
1782 })?;
1783 }
1784 }
1785 client
1786 .execute(&set.record_migration_sql(migration), &[])
1787 .await
1788 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1789 applied.push(migration.hash().to_string());
1790 }
1791 } else {
1792 let transaction = client
1793 .transaction()
1794 .await
1795 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1796 for migration in &pending {
1797 for statement in migration.statements() {
1798 if !statement.trim().is_empty() {
1799 transaction.execute(statement, &[]).await.map_err(|error| {
1800 CliError::MigrationError(format!(
1801 "Migration '{}' failed: {error}",
1802 migration.hash()
1803 ))
1804 })?;
1805 }
1806 }
1807 transaction
1808 .execute(&set.record_migration_sql(migration), &[])
1809 .await
1810 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1811 applied.push(migration.hash().to_string());
1812 }
1813 transaction
1814 .commit()
1815 .await
1816 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1817 }
1818
1819 Ok(MigrationResult {
1820 applied_count: applied.len(),
1821 applied_migrations: applied,
1822 })
1823}
1824
1825#[cfg(feature = "libsql")]
1830fn execute_libsql_local_statements(path: &str, statements: &[String]) -> Result<(), CliError> {
1831 let rt = tokio::runtime::Builder::new_current_thread()
1832 .enable_all()
1833 .build()
1834 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
1835
1836 rt.block_on(execute_libsql_local_inner(path, statements))
1837}
1838
1839#[cfg(feature = "libsql")]
1840async fn execute_libsql_local_inner(path: &str, statements: &[String]) -> Result<(), CliError> {
1841 let db = libsql::Builder::new_local(path)
1842 .build()
1843 .await
1844 .map_err(|e| {
1845 CliError::ConnectionError(format!("Failed to open LibSQL database '{path}': {e}"))
1846 })?;
1847
1848 let conn = db
1849 .connect()
1850 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
1851
1852 let tx = conn
1853 .transaction()
1854 .await
1855 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1856
1857 for stmt in statements {
1858 let s = stmt.trim();
1859 if s.is_empty() {
1860 continue;
1861 }
1862 if let Err(e) = tx.execute(s, ()).await {
1863 tx.rollback().await.ok();
1864 return Err(CliError::MigrationError(format!(
1865 "Statement failed: {e}\n{s}"
1866 )));
1867 }
1868 }
1869
1870 tx.commit()
1871 .await
1872 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1873
1874 Ok(())
1875}
1876
1877#[cfg(feature = "libsql")]
1878fn run_libsql_local_migrations(set: &Migrations, path: &str) -> Result<MigrationResult, CliError> {
1879 let rt = tokio::runtime::Builder::new_current_thread()
1880 .enable_all()
1881 .build()
1882 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
1883
1884 rt.block_on(run_libsql_local_inner(set, path))
1885}
1886
1887#[cfg(feature = "libsql")]
1888fn inspect_libsql_local_migrations(
1889 set: &Migrations,
1890 path: &str,
1891) -> Result<MigrationPlan, CliError> {
1892 let rt = tokio::runtime::Builder::new_current_thread()
1893 .enable_all()
1894 .build()
1895 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
1896
1897 rt.block_on(inspect_libsql_local_inner(set, path))
1898}
1899
1900#[cfg(feature = "libsql")]
1901async fn inspect_libsql_local_inner(
1902 set: &Migrations,
1903 path: &str,
1904) -> Result<MigrationPlan, CliError> {
1905 let db = libsql::Builder::new_local(path)
1906 .build()
1907 .await
1908 .map_err(|e| {
1909 CliError::ConnectionError(format!("Failed to open LibSQL database '{path}': {e}"))
1910 })?;
1911
1912 let conn = db
1913 .connect()
1914 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
1915
1916 ensure_sqlite_tracking_table_libsql(&conn, set).await?;
1917 let applied = query_applied_records_libsql(&conn, set).await?;
1918 build_migration_plan(set, &applied)
1919}
1920
1921#[cfg(feature = "libsql")]
1922async fn run_libsql_local_inner(set: &Migrations, path: &str) -> Result<MigrationResult, CliError> {
1923 let db = libsql::Builder::new_local(path)
1924 .build()
1925 .await
1926 .map_err(|e| {
1927 CliError::ConnectionError(format!("Failed to open LibSQL database '{path}': {e}"))
1928 })?;
1929
1930 let conn = db
1931 .connect()
1932 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
1933
1934 ensure_sqlite_tracking_table_libsql(&conn, set).await?;
1935 let tx = conn
1936 .transaction_with_behavior(libsql::TransactionBehavior::Immediate)
1937 .await
1938 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1939 let applied_names = query_applied_names_libsql(&tx, set).await?;
1940
1941 let pending: Vec<_> = set.pending(&applied_names).collect();
1943 if pending.is_empty() {
1944 tx.commit()
1945 .await
1946 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1947 return Ok(MigrationResult {
1948 applied_count: 0,
1949 applied_migrations: vec![],
1950 });
1951 }
1952
1953 let mut applied = Vec::new();
1954 for migration in &pending {
1955 for stmt in migration.statements() {
1956 if !stmt.trim().is_empty()
1957 && let Err(e) = tx.execute(stmt, ()).await
1958 {
1959 tx.rollback().await.ok();
1960 return Err(CliError::MigrationError(format!(
1961 "Migration '{}' failed: {}",
1962 migration.hash(),
1963 e
1964 )));
1965 }
1966 }
1967 if let Err(e) = tx.execute(&set.record_migration_sql(migration), ()).await {
1968 tx.rollback().await.ok();
1969 return Err(CliError::MigrationError(e.to_string()));
1970 }
1971 applied.push(migration.hash().to_string());
1972 }
1973
1974 tx.commit()
1975 .await
1976 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1977
1978 Ok(MigrationResult {
1979 applied_count: applied.len(),
1980 applied_migrations: applied,
1981 })
1982}
1983
1984#[cfg(feature = "libsql")]
1985async fn query_applied_names_libsql(
1986 conn: &libsql::Connection,
1987 set: &Migrations,
1988) -> Result<Vec<String>, CliError> {
1989 let mut rows = conn
1990 .query(&set.applied_names_sql(), ())
1991 .await
1992 .map_err(|error| CliError::MigrationError(error.to_string()))?;
1993
1994 let mut names = Vec::new();
1995 while let Some(row) = rows
1996 .next()
1997 .await
1998 .map_err(|error| CliError::MigrationError(error.to_string()))?
1999 {
2000 names.push(
2001 row.get::<String>(0)
2002 .map_err(|error| CliError::MigrationError(error.to_string()))?,
2003 );
2004 }
2005
2006 Ok(names)
2007}
2008
2009#[cfg(feature = "libsql")]
2010async fn query_applied_records_libsql(
2011 conn: &libsql::Connection,
2012 set: &Migrations,
2013) -> Result<Vec<AppliedMigrationRecord>, CliError> {
2014 let sql = format!(
2015 r#"SELECT hash, "name" FROM {} WHERE "name" IS NOT NULL ORDER BY id;"#,
2016 set.table_ident_sql()
2017 );
2018 let mut rows = conn
2019 .query(&sql, ())
2020 .await
2021 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2022
2023 let mut applied = Vec::new();
2024 while let Some(row) = rows
2025 .next()
2026 .await
2027 .map_err(|error| CliError::MigrationError(error.to_string()))?
2028 {
2029 let hash = row
2030 .get::<String>(0)
2031 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2032 let name = row
2033 .get::<String>(1)
2034 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2035 applied.push(AppliedMigrationRecord { hash, name });
2036 }
2037
2038 Ok(applied)
2039}
2040
2041#[cfg(feature = "turso")]
2046fn execute_turso_statements(
2047 url: &str,
2048 auth_token: Option<&str>,
2049 statements: &[String],
2050) -> Result<(), CliError> {
2051 let rt = tokio::runtime::Builder::new_current_thread()
2052 .enable_all()
2053 .build()
2054 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
2055
2056 rt.block_on(execute_turso_inner(url, auth_token, statements))
2057}
2058
2059#[cfg(feature = "turso")]
2060async fn execute_turso_inner(
2061 url: &str,
2062 auth_token: Option<&str>,
2063 statements: &[String],
2064) -> Result<(), CliError> {
2065 let builder =
2066 libsql::Builder::new_remote(url.to_string(), auth_token.unwrap_or("").to_string());
2067
2068 let db = builder.build().await.map_err(|e| {
2069 CliError::ConnectionError(format!("Failed to connect to Turso '{url}': {e}"))
2070 })?;
2071
2072 let conn = db
2073 .connect()
2074 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
2075
2076 let tx = conn
2077 .transaction()
2078 .await
2079 .map_err(|e| CliError::MigrationError(e.to_string()))?;
2080
2081 for stmt in statements {
2082 let s = stmt.trim();
2083 if s.is_empty() {
2084 continue;
2085 }
2086 if let Err(e) = tx.execute(s, ()).await {
2087 tx.rollback().await.ok();
2088 return Err(CliError::MigrationError(format!(
2089 "Statement failed: {e}\n{s}"
2090 )));
2091 }
2092 }
2093
2094 tx.commit()
2095 .await
2096 .map_err(|e| CliError::MigrationError(e.to_string()))?;
2097
2098 Ok(())
2099}
2100
2101#[cfg(feature = "turso")]
2102fn run_turso_migrations(
2103 set: &Migrations,
2104 url: &str,
2105 auth_token: Option<&str>,
2106) -> Result<MigrationResult, CliError> {
2107 let rt = tokio::runtime::Builder::new_current_thread()
2108 .enable_all()
2109 .build()
2110 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
2111
2112 rt.block_on(run_turso_inner(set, url, auth_token))
2113}
2114
2115#[cfg(feature = "turso")]
2116fn inspect_turso_migrations(
2117 set: &Migrations,
2118 url: &str,
2119 auth_token: Option<&str>,
2120) -> Result<MigrationPlan, CliError> {
2121 let rt = tokio::runtime::Builder::new_current_thread()
2122 .enable_all()
2123 .build()
2124 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
2125
2126 rt.block_on(inspect_turso_inner(set, url, auth_token))
2127}
2128
2129#[cfg(feature = "turso")]
2130async fn inspect_turso_inner(
2131 set: &Migrations,
2132 url: &str,
2133 auth_token: Option<&str>,
2134) -> Result<MigrationPlan, CliError> {
2135 let builder =
2136 libsql::Builder::new_remote(url.to_string(), auth_token.unwrap_or("").to_string());
2137
2138 let db = builder.build().await.map_err(|e| {
2139 CliError::ConnectionError(format!("Failed to connect to Turso '{url}': {e}"))
2140 })?;
2141
2142 let conn = db
2143 .connect()
2144 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
2145
2146 ensure_sqlite_tracking_table_libsql(&conn, set).await?;
2147 let applied = query_applied_records_turso(&conn, set).await?;
2148 build_migration_plan(set, &applied)
2149}
2150
2151#[cfg(feature = "turso")]
2152async fn run_turso_inner(
2153 set: &Migrations,
2154 url: &str,
2155 auth_token: Option<&str>,
2156) -> Result<MigrationResult, CliError> {
2157 let builder =
2158 libsql::Builder::new_remote(url.to_string(), auth_token.unwrap_or("").to_string());
2159
2160 let db = builder.build().await.map_err(|e| {
2161 CliError::ConnectionError(format!("Failed to connect to Turso '{url}': {e}"))
2162 })?;
2163
2164 let conn = db
2165 .connect()
2166 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
2167
2168 ensure_sqlite_tracking_table_libsql(&conn, set).await?;
2169 let tx = conn
2170 .transaction_with_behavior(libsql::TransactionBehavior::Immediate)
2171 .await
2172 .map_err(|e| CliError::MigrationError(e.to_string()))?;
2173 let applied_names = query_applied_names_turso(&tx, set).await?;
2174
2175 let pending: Vec<_> = set.pending(&applied_names).collect();
2177 if pending.is_empty() {
2178 tx.commit()
2179 .await
2180 .map_err(|e| CliError::MigrationError(e.to_string()))?;
2181 return Ok(MigrationResult {
2182 applied_count: 0,
2183 applied_migrations: vec![],
2184 });
2185 }
2186
2187 let mut applied = Vec::new();
2188 for migration in &pending {
2189 for stmt in migration.statements() {
2190 if !stmt.trim().is_empty()
2191 && let Err(e) = tx.execute(stmt, ()).await
2192 {
2193 tx.rollback().await.ok();
2194 return Err(CliError::MigrationError(format!(
2195 "Migration '{}' failed: {}",
2196 migration.hash(),
2197 e
2198 )));
2199 }
2200 }
2201 if let Err(e) = tx.execute(&set.record_migration_sql(migration), ()).await {
2202 tx.rollback().await.ok();
2203 return Err(CliError::MigrationError(e.to_string()));
2204 }
2205 applied.push(migration.hash().to_string());
2206 }
2207
2208 tx.commit()
2209 .await
2210 .map_err(|e| CliError::MigrationError(e.to_string()))?;
2211
2212 Ok(MigrationResult {
2213 applied_count: applied.len(),
2214 applied_migrations: applied,
2215 })
2216}
2217
2218#[cfg(feature = "turso")]
2219async fn query_applied_names_turso(
2220 conn: &libsql::Connection,
2221 set: &Migrations,
2222) -> Result<Vec<String>, CliError> {
2223 let mut rows = conn
2224 .query(&set.applied_names_sql(), ())
2225 .await
2226 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2227
2228 let mut names = Vec::new();
2229 while let Some(row) = rows
2230 .next()
2231 .await
2232 .map_err(|error| CliError::MigrationError(error.to_string()))?
2233 {
2234 names.push(
2235 row.get::<String>(0)
2236 .map_err(|error| CliError::MigrationError(error.to_string()))?,
2237 );
2238 }
2239
2240 Ok(names)
2241}
2242
2243#[cfg(feature = "turso")]
2244async fn query_applied_records_turso(
2245 conn: &libsql::Connection,
2246 set: &Migrations,
2247) -> Result<Vec<AppliedMigrationRecord>, CliError> {
2248 let sql = format!(
2249 r#"SELECT hash, "name" FROM {} WHERE "name" IS NOT NULL ORDER BY id;"#,
2250 set.table_ident_sql()
2251 );
2252 let mut rows = conn
2253 .query(&sql, ())
2254 .await
2255 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2256
2257 let mut applied = Vec::new();
2258 while let Some(row) = rows
2259 .next()
2260 .await
2261 .map_err(|error| CliError::MigrationError(error.to_string()))?
2262 {
2263 let hash = row
2264 .get::<String>(0)
2265 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2266 let name = row
2267 .get::<String>(1)
2268 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2269 applied.push(AppliedMigrationRecord { hash, name });
2270 }
2271
2272 Ok(applied)
2273}
2274
2275#[derive(Debug)]
2281pub struct IntrospectResult {
2282 pub schema_code: String,
2284 pub table_count: usize,
2286 pub index_count: usize,
2288 pub view_count: usize,
2290 pub warnings: Vec<String>,
2292 pub snapshot: Snapshot,
2294 pub snapshot_path: std::path::PathBuf,
2296}
2297
2298#[allow(clippy::too_many_arguments)]
2308pub fn run_introspection(
2309 credentials: &Credentials,
2310 dialect: Dialect,
2311 out_dir: &Path,
2312 init_metadata: bool,
2313 breakpoints: bool,
2314 introspect_casing: Option<IntrospectCasing>,
2315 filters: &SnapshotFilters,
2316 migrations_table: &str,
2317 migrations_schema: &str,
2318) -> Result<IntrospectResult, CliError> {
2319 use drizzle_migrations::words::generate_migration_tag;
2320
2321 let mut result = introspect_database(credentials, dialect)?;
2323 apply_snapshot_filters(&mut result.snapshot, dialect, filters)?;
2324 if !filters.is_empty() || introspect_casing.is_some() {
2325 regenerate_schema_from_snapshot(&mut result, dialect, introspect_casing);
2326 }
2327
2328 let schema_path = out_dir.join("schema.rs");
2330 if let Some(parent) = schema_path.parent() {
2331 std::fs::create_dir_all(parent).map_err(|e| {
2332 CliError::Other(format!(
2333 "Failed to create output directory '{}': {}",
2334 parent.display(),
2335 e
2336 ))
2337 })?;
2338 }
2339 std::fs::write(&schema_path, &result.schema_code).map_err(|e| {
2340 CliError::Other(format!(
2341 "Failed to write schema file '{}': {}",
2342 schema_path.display(),
2343 e
2344 ))
2345 })?;
2346
2347 let journal_path = out_dir.join("meta").join("_journal.json");
2348 if journal_path.exists() {
2349 return Err(CliError::Other(
2350 "Detected old drizzle-kit migration folders. Upgrade them before writing new migrations."
2351 .to_string(),
2352 ));
2353 }
2354
2355 let tag = generate_migration_tag(None);
2357
2358 let migration_dir = out_dir.join(&tag);
2360 std::fs::create_dir_all(&migration_dir).map_err(|e| {
2361 CliError::Other(format!(
2362 "Failed to create migration directory '{}': {}",
2363 migration_dir.display(),
2364 e
2365 ))
2366 })?;
2367
2368 let snapshot_path = migration_dir.join("snapshot.json");
2370 result.snapshot.save(&snapshot_path).map_err(|e| {
2371 CliError::Other(format!(
2372 "Failed to write snapshot file '{}': {}",
2373 snapshot_path.display(),
2374 e
2375 ))
2376 })?;
2377
2378 let base_dialect = dialect.to_base();
2380 let empty_snapshot = Snapshot::empty(base_dialect);
2381 let sql_statements =
2382 generate_introspect_migration(&empty_snapshot, &result.snapshot, breakpoints)?;
2383
2384 let migration_sql_path = migration_dir.join("migration.sql");
2386 let sql_content = format_migration_sql(&sql_statements, breakpoints);
2387 std::fs::write(&migration_sql_path, &sql_content).map_err(|e| {
2388 CliError::Other(format!(
2389 "Failed to write migration file '{}': {}",
2390 migration_sql_path.display(),
2391 e
2392 ))
2393 })?;
2394
2395 result.snapshot_path = snapshot_path;
2397
2398 if init_metadata {
2399 apply_init_metadata(
2400 credentials,
2401 dialect,
2402 out_dir,
2403 migrations_table,
2404 migrations_schema,
2405 )?;
2406 }
2407
2408 Ok(result)
2409}
2410
2411#[allow(unused_variables)] fn apply_init_metadata(
2417 credentials: &Credentials,
2418 dialect: Dialect,
2419 out_dir: &Path,
2420 migrations_table: &str,
2421 migrations_schema: &str,
2422) -> Result<(), CliError> {
2423 #[cfg(any(
2424 feature = "rusqlite",
2425 feature = "libsql",
2426 feature = "turso",
2427 feature = "postgres-sync",
2428 feature = "tokio-postgres",
2429 feature = "d1-http",
2430 ))]
2431 let set = load_migration_set(dialect, out_dir, migrations_table, migrations_schema)?;
2432
2433 match credentials {
2434 #[cfg(feature = "rusqlite")]
2435 Credentials::Sqlite { path } => init_sqlite_metadata(path, &set),
2436
2437 #[cfg(not(feature = "rusqlite"))]
2438 Credentials::Sqlite { .. } => Err(CliError::MissingDriver {
2439 dialect: "SQLite",
2440 feature: "rusqlite",
2441 }),
2442
2443 #[cfg(any(feature = "libsql", feature = "turso"))]
2444 Credentials::Turso { url, auth_token } => {
2445 if is_local_libsql(url) {
2446 #[cfg(feature = "libsql")]
2447 {
2448 init_libsql_local_metadata(url, &set)
2449 }
2450 #[cfg(not(feature = "libsql"))]
2451 {
2452 Err(CliError::MissingDriver {
2453 dialect: "LibSQL (local)",
2454 feature: "libsql",
2455 })
2456 }
2457 } else {
2458 #[cfg(feature = "turso")]
2459 {
2460 init_turso_metadata(url, auth_token.as_deref(), &set)
2461 }
2462 #[cfg(not(feature = "turso"))]
2463 {
2464 let _ = auth_token;
2465 Err(CliError::MissingDriver {
2466 dialect: "Turso (remote)",
2467 feature: "turso",
2468 })
2469 }
2470 }
2471 }
2472
2473 #[cfg(all(not(feature = "turso"), not(feature = "libsql")))]
2474 Credentials::Turso { .. } => Err(CliError::MissingDriver {
2475 dialect: "Turso",
2476 feature: "turso or libsql",
2477 }),
2478
2479 Credentials::Postgres(creds) => {
2480 let _ = creds;
2481 core::cfg_select! {
2482 feature = "postgres-sync" => init_postgres_sync_metadata(creds, &set),
2483 feature = "tokio-postgres" => init_postgres_async_metadata(creds, &set),
2484 _ => Err(CliError::MissingDriver {
2485 dialect: "PostgreSQL",
2486 feature: "postgres-sync or tokio-postgres",
2487 }),
2488 }
2489 }
2490
2491 #[cfg(feature = "d1-http")]
2492 Credentials::D1 {
2493 account_id,
2494 database_id,
2495 token,
2496 } => d1_http::init_metadata(&set, account_id, database_id, token),
2497
2498 #[cfg(not(feature = "d1-http"))]
2499 Credentials::D1 { .. } => Err(CliError::MissingDriver {
2500 dialect: "Cloudflare D1 (HTTP)",
2501 feature: "d1-http",
2502 }),
2503
2504 Credentials::AwsDataApi { .. } => Err(CliError::UnsupportedForDriver {
2505 operation: "Migration metadata init against AWS Data API",
2506 driver: "aws-data-api",
2507 hint: "AWS RDS Data API schema ops are not yet wired into this CLI. Seed the \
2508 migrations table manually via `aws rds-data execute-statement` or \
2509 tokio-postgres.",
2510 }),
2511 }
2512}
2513
2514#[cfg(any(
2515 feature = "rusqlite",
2516 feature = "libsql",
2517 feature = "turso",
2518 feature = "postgres-sync",
2519 feature = "tokio-postgres",
2520 feature = "d1-http",
2521))]
2522pub(crate) fn validate_init_metadata(
2523 applied_names: &[String],
2524 set: &Migrations,
2525) -> Result<(), CliError> {
2526 if !applied_names.is_empty() {
2527 return Err(CliError::Other(
2528 "--init can't be used when database already has migrations set".into(),
2529 ));
2530 }
2531
2532 if set.all().len() > 1 {
2533 return Err(CliError::Other(
2534 "--init can't be used with existing migrations".into(),
2535 ));
2536 }
2537
2538 Ok(())
2539}
2540
2541#[cfg(feature = "rusqlite")]
2546fn init_sqlite_metadata(path: &str, set: &Migrations) -> Result<(), CliError> {
2547 let conn = rusqlite::Connection::open(path).map_err(|e| {
2548 CliError::ConnectionError(format!("Failed to open SQLite database '{path}': {e}"))
2549 })?;
2550
2551 ensure_sqlite_tracking_table(&conn, set)?;
2552
2553 let applied_names = query_applied_names_sqlite(&conn, set)?;
2554 validate_init_metadata(&applied_names, set)?;
2555
2556 let Some(first) = set.all().first() else {
2557 return Ok(());
2558 };
2559
2560 conn.execute(&set.record_migration_sql(first), [])
2561 .map_err(|e| CliError::MigrationError(e.to_string()))?;
2562
2563 Ok(())
2564}
2565
2566#[cfg(feature = "libsql")]
2567fn init_libsql_local_metadata(path: &str, set: &Migrations) -> Result<(), CliError> {
2568 let rt = tokio::runtime::Builder::new_current_thread()
2569 .enable_all()
2570 .build()
2571 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
2572
2573 rt.block_on(init_libsql_local_metadata_inner(path, set))
2574}
2575
2576#[cfg(feature = "libsql")]
2577async fn init_libsql_local_metadata_inner(path: &str, set: &Migrations) -> Result<(), CliError> {
2578 let db = libsql::Builder::new_local(path)
2579 .build()
2580 .await
2581 .map_err(|e| {
2582 CliError::ConnectionError(format!("Failed to open LibSQL database '{path}': {e}"))
2583 })?;
2584
2585 let conn = db
2586 .connect()
2587 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
2588
2589 ensure_sqlite_tracking_table_libsql(&conn, set).await?;
2590
2591 let applied_names = query_applied_names_libsql(&conn, set).await?;
2592 validate_init_metadata(&applied_names, set)?;
2593
2594 let Some(first) = set.all().first() else {
2595 return Ok(());
2596 };
2597
2598 conn.execute(&set.record_migration_sql(first), ())
2599 .await
2600 .map_err(|e| CliError::MigrationError(e.to_string()))?;
2601
2602 Ok(())
2603}
2604
2605#[cfg(feature = "turso")]
2606fn init_turso_metadata(
2607 url: &str,
2608 auth_token: Option<&str>,
2609 set: &Migrations,
2610) -> Result<(), CliError> {
2611 let rt = tokio::runtime::Builder::new_current_thread()
2612 .enable_all()
2613 .build()
2614 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
2615
2616 rt.block_on(init_turso_metadata_inner(url, auth_token, set))
2617}
2618
2619#[cfg(feature = "turso")]
2620async fn init_turso_metadata_inner(
2621 url: &str,
2622 auth_token: Option<&str>,
2623 set: &Migrations,
2624) -> Result<(), CliError> {
2625 let builder =
2626 libsql::Builder::new_remote(url.to_string(), auth_token.unwrap_or("").to_string());
2627
2628 let db = builder.build().await.map_err(|e| {
2629 CliError::ConnectionError(format!("Failed to connect to Turso '{url}': {e}"))
2630 })?;
2631
2632 let conn = db
2633 .connect()
2634 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
2635
2636 ensure_sqlite_tracking_table_libsql(&conn, set).await?;
2637
2638 let applied_names = query_applied_names_turso(&conn, set).await?;
2639 validate_init_metadata(&applied_names, set)?;
2640
2641 let Some(first) = set.all().first() else {
2642 return Ok(());
2643 };
2644
2645 conn.execute(&set.record_migration_sql(first), ())
2646 .await
2647 .map_err(|e| CliError::MigrationError(e.to_string()))?;
2648
2649 Ok(())
2650}
2651
2652#[cfg(feature = "postgres-sync")]
2653fn init_postgres_sync_metadata(creds: &PostgresCreds, set: &Migrations) -> Result<(), CliError> {
2654 let mut client = connect_postgres_sync(creds)?;
2655
2656 if let Some(schema_sql) = set.create_schema_sql() {
2657 client
2658 .execute(&schema_sql, &[])
2659 .map_err(|e| CliError::MigrationError(e.to_string()))?;
2660 }
2661
2662 ensure_postgres_tracking_table_sync(&mut client, set)?;
2663
2664 let rows = client
2665 .query(&set.applied_names_sql(), &[])
2666 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2667 let applied_names = rows
2668 .iter()
2669 .map(|row| row.try_get(0))
2670 .collect::<Result<Vec<String>, _>>()
2671 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2672
2673 validate_init_metadata(&applied_names, set)?;
2674
2675 let Some(first) = set.all().first() else {
2676 return Ok(());
2677 };
2678
2679 client
2680 .execute(&set.record_migration_sql(first), &[])
2681 .map_err(|e| CliError::MigrationError(e.to_string()))?;
2682
2683 Ok(())
2684}
2685
2686#[cfg(all(feature = "tokio-postgres", not(feature = "postgres-sync")))]
2687fn init_postgres_async_metadata(creds: &PostgresCreds, set: &Migrations) -> Result<(), CliError> {
2688 let rt = tokio::runtime::Builder::new_current_thread()
2689 .enable_all()
2690 .build()
2691 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {}", e)))?;
2692
2693 rt.block_on(init_postgres_async_inner(creds, set))
2694}
2695
2696#[cfg(all(feature = "tokio-postgres", not(feature = "postgres-sync")))]
2697async fn init_postgres_async_inner(
2698 creds: &PostgresCreds,
2699 set: &Migrations,
2700) -> Result<(), CliError> {
2701 let client = connect_postgres_async(creds).await?;
2702
2703 if let Some(schema_sql) = set.create_schema_sql() {
2704 client
2705 .execute(&schema_sql, &[])
2706 .await
2707 .map_err(|e| CliError::MigrationError(e.to_string()))?;
2708 }
2709
2710 ensure_postgres_tracking_table_async(&client, set).await?;
2711
2712 let rows = client
2713 .query(&set.applied_names_sql(), &[])
2714 .await
2715 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2716 let applied_names = rows
2717 .iter()
2718 .map(|row| row.try_get(0))
2719 .collect::<Result<Vec<String>, _>>()
2720 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2721
2722 validate_init_metadata(&applied_names, set)?;
2723
2724 let Some(first) = set.all().first() else {
2725 return Ok(());
2726 };
2727
2728 client
2729 .execute(&set.record_migration_sql(first), &[])
2730 .await
2731 .map_err(|e| CliError::MigrationError(e.to_string()))?;
2732
2733 Ok(())
2734}
2735
2736fn generate_introspect_migration(
2738 prev: &Snapshot,
2739 current: &Snapshot,
2740 breakpoints: bool,
2741) -> Result<Vec<String>, CliError> {
2742 match (prev, current) {
2743 (Snapshot::Sqlite(prev_snap), Snapshot::Sqlite(curr_snap)) => {
2744 use drizzle_migrations::sqlite::diff_snapshots;
2745 use drizzle_migrations::sqlite::statements::SqliteGenerator;
2746
2747 let diff = diff_snapshots(prev_snap, curr_snap);
2748 let generator = SqliteGenerator::new().with_breakpoints(breakpoints);
2749 Ok(generator.generate_migration(&diff))
2750 }
2751 (Snapshot::Postgres(prev_snap), Snapshot::Postgres(curr_snap)) => {
2752 use drizzle_migrations::postgres::diff_full_snapshots;
2753 use drizzle_migrations::postgres::statements::PostgresGenerator;
2754
2755 let diff = diff_full_snapshots(prev_snap, curr_snap);
2756 let generator = PostgresGenerator::new().with_breakpoints(breakpoints);
2757 Ok(generator.generate(&diff.diffs))
2758 }
2759 _ => Err(CliError::DialectMismatch),
2760 }
2761}
2762
2763fn format_migration_sql(sql_statements: &[String], breakpoints: bool) -> String {
2764 if sql_statements.is_empty() {
2765 "-- No tables to create (empty database)\n".to_string()
2766 } else if breakpoints {
2767 sql_statements.join("\n--> statement-breakpoint\n")
2768 } else {
2769 sql_statements.join("\n\n")
2770 }
2771}
2772
2773fn regenerate_schema_from_snapshot(
2774 result: &mut IntrospectResult,
2775 dialect: Dialect,
2776 introspect_casing: Option<IntrospectCasing>,
2777) {
2778 match (&result.snapshot, dialect) {
2779 (Snapshot::Sqlite(snap), Dialect::Sqlite | Dialect::Turso) => {
2780 use drizzle_migrations::sqlite::SQLiteDDL;
2781 use drizzle_migrations::sqlite::codegen::{
2782 CodegenOptions, FieldCasing, generate_rust_schema,
2783 };
2784
2785 let field_casing = match introspect_casing {
2786 Some(IntrospectCasing::Camel) => FieldCasing::Camel,
2787 Some(IntrospectCasing::Preserve) => FieldCasing::Preserve,
2788 None => FieldCasing::Snake,
2789 };
2790
2791 let ddl = SQLiteDDL::from_entities(snap.ddl.clone());
2792 let generated = generate_rust_schema(
2793 &ddl,
2794 &CodegenOptions {
2795 module_doc: Some("Schema introspected from filtered database objects".into()),
2796 include_schema: true,
2797 schema_name: "Schema".into(),
2798 use_pub: true,
2799 field_casing,
2800 },
2801 );
2802
2803 result.schema_code = generated.code;
2804 result.table_count = generated.tables.len();
2805 result.index_count = generated.indexes.len();
2806 result.view_count = ddl.views.list().len();
2807 result.warnings = generated.warnings;
2808 }
2809 (Snapshot::Postgres(snap), Dialect::Postgresql) => {
2810 use drizzle_migrations::postgres::PostgresDDL;
2811 use drizzle_migrations::postgres::codegen::{
2812 CodegenOptions, FieldCasing, generate_rust_schema,
2813 };
2814
2815 let field_casing = match introspect_casing {
2816 Some(IntrospectCasing::Camel) => FieldCasing::Camel,
2817 Some(IntrospectCasing::Preserve) => FieldCasing::Preserve,
2818 None => FieldCasing::Snake,
2819 };
2820
2821 let ddl = PostgresDDL::from_entities(snap.ddl.clone());
2822 let generated = generate_rust_schema(
2823 &ddl,
2824 &CodegenOptions {
2825 module_doc: Some("Schema introspected from filtered database objects".into()),
2826 include_schema: true,
2827 schema_name: "Schema".into(),
2828 use_pub: true,
2829 field_casing,
2830 },
2831 );
2832
2833 result.schema_code = generated.code;
2834 result.table_count = generated.tables.len();
2835 result.index_count = generated.indexes.len();
2836 result.view_count = generated.views.len();
2837 result.warnings = generated.warnings;
2838 }
2839 _ => {}
2840 }
2841}
2842
2843fn introspect_database(
2845 credentials: &Credentials,
2846 dialect: Dialect,
2847) -> Result<IntrospectResult, CliError> {
2848 match dialect {
2849 Dialect::Sqlite | Dialect::Turso => introspect_sqlite_dialect(credentials),
2850 Dialect::Postgresql => introspect_postgres_dialect(credentials),
2851 }
2852}
2853
2854fn introspect_sqlite_dialect(credentials: &Credentials) -> Result<IntrospectResult, CliError> {
2856 match credentials {
2857 #[cfg(feature = "rusqlite")]
2858 Credentials::Sqlite { path } => introspect_rusqlite(path),
2859
2860 #[cfg(not(feature = "rusqlite"))]
2861 Credentials::Sqlite { .. } => Err(CliError::MissingDriver {
2862 dialect: "SQLite",
2863 feature: "rusqlite",
2864 }),
2865
2866 #[cfg(any(feature = "libsql", feature = "turso"))]
2867 Credentials::Turso { url, auth_token } => {
2868 if is_local_libsql(url) {
2869 #[cfg(feature = "libsql")]
2870 {
2871 introspect_libsql_local(url)
2872 }
2873 #[cfg(not(feature = "libsql"))]
2874 {
2875 Err(CliError::MissingDriver {
2876 dialect: "LibSQL (local)",
2877 feature: "libsql",
2878 })
2879 }
2880 } else {
2881 #[cfg(feature = "turso")]
2882 {
2883 introspect_turso(url, auth_token.as_deref())
2884 }
2885 #[cfg(not(feature = "turso"))]
2886 {
2887 let _ = auth_token;
2888 Err(CliError::MissingDriver {
2889 dialect: "Turso (remote)",
2890 feature: "turso",
2891 })
2892 }
2893 }
2894 }
2895
2896 #[cfg(all(not(feature = "turso"), not(feature = "libsql")))]
2897 Credentials::Turso { .. } => Err(CliError::MissingDriver {
2898 dialect: "Turso",
2899 feature: "turso or libsql",
2900 }),
2901
2902 _ => Err(CliError::Other(
2903 "SQLite introspection requires sqlite path or turso credentials".into(),
2904 )),
2905 }
2906}
2907
2908fn introspect_postgres_dialect(credentials: &Credentials) -> Result<IntrospectResult, CliError> {
2910 match credentials {
2911 Credentials::Postgres(creds) => {
2912 let _ = creds;
2913 core::cfg_select! {
2914 feature = "postgres-sync" => introspect_postgres_sync(creds),
2915 feature = "tokio-postgres" => introspect_postgres_async(creds),
2916 _ => Err(CliError::MissingDriver {
2917 dialect: "PostgreSQL",
2918 feature: "postgres-sync or tokio-postgres",
2919 }),
2920 }
2921 }
2922
2923 _ => Err(CliError::Other(
2924 "PostgreSQL introspection requires postgres credentials".into(),
2925 )),
2926 }
2927}
2928
2929#[cfg(any(feature = "rusqlite", feature = "libsql", feature = "turso"))]
2934struct SqliteRawData {
2935 tables: Vec<(String, Option<String>)>,
2936 raw_columns: Vec<drizzle_migrations::sqlite::introspect::RawColumnInfo>,
2937 all_indexes: Vec<drizzle_migrations::sqlite::introspect::RawIndexInfo>,
2938 all_index_columns: Vec<drizzle_migrations::sqlite::introspect::RawIndexColumn>,
2939 all_fks: Vec<drizzle_migrations::sqlite::introspect::RawForeignKey>,
2940 all_views: Vec<drizzle_migrations::sqlite::introspect::RawViewInfo>,
2941}
2942
2943#[cfg(any(feature = "rusqlite", feature = "libsql", feature = "turso"))]
2944impl SqliteRawData {
2945 const fn empty() -> Self {
2946 Self {
2947 tables: Vec::new(),
2948 raw_columns: Vec::new(),
2949 all_indexes: Vec::new(),
2950 all_index_columns: Vec::new(),
2951 all_fks: Vec::new(),
2952 all_views: Vec::new(),
2953 }
2954 }
2955}
2956
2957#[cfg(feature = "rusqlite")]
2958fn query_rusqlite_tables_and_columns(
2959 conn: &rusqlite::Connection,
2960 raw: &mut SqliteRawData,
2961) -> Result<(), CliError> {
2962 use drizzle_migrations::sqlite::introspect::{RawColumnInfo, queries};
2963
2964 let mut tables_stmt = conn
2965 .prepare(queries::TABLES_QUERY)
2966 .map_err(|e| CliError::Other(format!("Failed to prepare tables query: {e}")))?;
2967
2968 raw.tables = tables_stmt
2969 .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
2970 .map_err(|e| CliError::Other(e.to_string()))?
2971 .collect::<Result<Vec<_>, _>>()
2972 .map_err(|e| CliError::Other(e.to_string()))?;
2973
2974 let mut columns_stmt = conn
2975 .prepare(queries::COLUMNS_QUERY)
2976 .map_err(|e| CliError::Other(format!("Failed to prepare columns query: {e}")))?;
2977
2978 raw.raw_columns = columns_stmt
2979 .query_map([], |row| {
2980 Ok(RawColumnInfo {
2981 table: row.get(0)?,
2982 cid: row.get(1)?,
2983 name: row.get(2)?,
2984 column_type: row.get(3)?,
2985 not_null: row.get(4)?,
2986 default_value: row.get(5)?,
2987 pk: row.get(6)?,
2988 hidden: row.get(7)?,
2989 sql: row.get(8)?,
2990 })
2991 })
2992 .map_err(|e| CliError::Other(e.to_string()))?
2993 .collect::<Result<Vec<_>, _>>()
2994 .map_err(|e| CliError::Other(e.to_string()))?;
2995
2996 Ok(())
2997}
2998
2999#[cfg(feature = "rusqlite")]
3000fn query_rusqlite_indexes_and_foreign_keys(
3001 conn: &rusqlite::Connection,
3002 raw: &mut SqliteRawData,
3003) -> Result<(), CliError> {
3004 use drizzle_migrations::sqlite::introspect::{
3005 RawForeignKey, RawIndexColumn, RawIndexInfo, queries,
3006 };
3007
3008 let mut indexes_stmt = conn
3009 .prepare(queries::INDEXES_QUERY)
3010 .map_err(|e| CliError::Other(e.to_string()))?;
3011 raw.all_indexes = indexes_stmt
3012 .query_map([], |row| {
3013 Ok(RawIndexInfo {
3014 table: row.get(0)?,
3015 name: row.get(1)?,
3016 unique: row.get::<_, i32>(2)? != 0,
3017 origin: row.get(3)?,
3018 partial: row.get::<_, i32>(4)? != 0,
3019 })
3020 })
3021 .map_err(|e| CliError::Other(e.to_string()))?
3022 .collect::<Result<Vec<_>, _>>()
3023 .map_err(|e| CliError::Other(e.to_string()))?;
3024
3025 let mut columns_stmt = conn
3026 .prepare(queries::INDEX_COLUMNS_QUERY)
3027 .map_err(|e| CliError::Other(e.to_string()))?;
3028 raw.all_index_columns = columns_stmt
3029 .query_map([], |row| {
3030 Ok(RawIndexColumn {
3031 index_name: row.get(0)?,
3032 seqno: row.get(1)?,
3033 cid: row.get(2)?,
3034 name: row.get(3)?,
3035 desc: row.get::<_, i32>(4)? != 0,
3036 coll: row.get(5)?,
3037 key: row.get::<_, i32>(6)? != 0,
3038 })
3039 })
3040 .map_err(|e| CliError::Other(e.to_string()))?
3041 .collect::<Result<Vec<_>, _>>()
3042 .map_err(|e| CliError::Other(e.to_string()))?;
3043
3044 let mut foreign_keys_stmt = conn
3045 .prepare(queries::FOREIGN_KEYS_QUERY)
3046 .map_err(|e| CliError::Other(e.to_string()))?;
3047 raw.all_fks = foreign_keys_stmt
3048 .query_map([], |row| {
3049 Ok(RawForeignKey {
3050 table: row.get(0)?,
3051 id: row.get(1)?,
3052 seq: row.get(2)?,
3053 to_table: row.get(3)?,
3054 from_column: row.get(4)?,
3055 to_column: row.get(5)?,
3056 on_update: row.get(6)?,
3057 on_delete: row.get(7)?,
3058 r#match: row.get(8)?,
3059 })
3060 })
3061 .map_err(|e| CliError::Other(e.to_string()))?
3062 .collect::<Result<Vec<_>, _>>()
3063 .map_err(|e| CliError::Other(e.to_string()))?;
3064
3065 Ok(())
3066}
3067
3068#[cfg(feature = "rusqlite")]
3069fn query_rusqlite_views_and_view_columns(
3070 conn: &rusqlite::Connection,
3071 raw: &mut SqliteRawData,
3072) -> Result<(), CliError> {
3073 use drizzle_migrations::sqlite::introspect::{RawColumnInfo, RawViewInfo, queries};
3074
3075 let mut views_stmt = conn
3076 .prepare(queries::VIEWS_QUERY)
3077 .map_err(|e| CliError::Other(e.to_string()))?;
3078 let view_iter = views_stmt
3079 .query_map([], |row| {
3080 Ok(RawViewInfo {
3081 name: row.get(0)?,
3082 sql: row.get(1)?,
3083 })
3084 })
3085 .map_err(|e| CliError::Other(e.to_string()))?;
3086 raw.all_views.extend(
3087 view_iter
3088 .collect::<Result<Vec<_>, _>>()
3089 .map_err(|e| CliError::Other(e.to_string()))?,
3090 );
3091
3092 let mut view_cols_stmt = conn
3093 .prepare(queries::VIEW_COLUMNS_QUERY)
3094 .map_err(|e| CliError::Other(e.to_string()))?;
3095 let col_iter = view_cols_stmt
3096 .query_map([], |row| {
3097 Ok(RawColumnInfo {
3098 table: row.get(0)?,
3099 cid: row.get(1)?,
3100 name: row.get(2)?,
3101 column_type: row.get(3)?,
3102 not_null: row.get::<_, i32>(4)? != 0,
3103 default_value: row.get(5)?,
3104 pk: row.get(6)?,
3105 hidden: row.get(7)?,
3106 sql: row.get(8)?,
3107 })
3108 })
3109 .map_err(|e| CliError::Other(e.to_string()))?;
3110 raw.raw_columns.extend(
3111 col_iter
3112 .collect::<Result<Vec<_>, _>>()
3113 .map_err(|e| CliError::Other(e.to_string()))?,
3114 );
3115 Ok(())
3116}
3117
3118#[cfg(feature = "rusqlite")]
3119fn query_rusqlite_raw(conn: &rusqlite::Connection) -> Result<SqliteRawData, CliError> {
3120 let mut raw = SqliteRawData::empty();
3121 query_rusqlite_tables_and_columns(conn, &mut raw)?;
3122 query_rusqlite_indexes_and_foreign_keys(conn, &mut raw)?;
3123 query_rusqlite_views_and_view_columns(conn, &mut raw)?;
3124 Ok(raw)
3125}
3126
3127#[cfg(any(feature = "rusqlite", feature = "libsql", feature = "turso"))]
3128fn build_sqlite_ddl(raw: SqliteRawData) -> drizzle_migrations::sqlite::SQLiteDDL {
3129 drizzle_migrations::sqlite::introspect::assemble_ddl(
3130 drizzle_migrations::sqlite::introspect::RawIntrospection {
3131 tables: raw.tables,
3132 columns: raw.raw_columns,
3133 indexes: raw.all_indexes,
3134 index_columns: raw.all_index_columns,
3135 foreign_keys: raw.all_fks,
3136 views: raw.all_views,
3137 },
3138 )
3139}
3140
3141#[cfg(feature = "rusqlite")]
3142fn introspect_rusqlite(path: &str) -> Result<IntrospectResult, CliError> {
3143 use drizzle_migrations::sqlite::codegen::{CodegenOptions, FieldCasing, generate_rust_schema};
3144
3145 let conn = rusqlite::Connection::open(path).map_err(|e| {
3146 CliError::ConnectionError(format!("Failed to open SQLite database '{path}': {e}"))
3147 })?;
3148
3149 let raw = query_rusqlite_raw(&conn)?;
3150 let ddl = build_sqlite_ddl(raw);
3151
3152 let options = CodegenOptions {
3153 module_doc: Some(format!("Schema introspected from {path}")),
3154 include_schema: true,
3155 schema_name: "Schema".to_string(),
3156 use_pub: true,
3157 field_casing: FieldCasing::default(),
3158 };
3159
3160 let generated = generate_rust_schema(&ddl, &options);
3161
3162 let mut sqlite_snapshot = drizzle_migrations::sqlite::SQLiteSnapshot::new();
3163 for entity in ddl.to_entities() {
3164 sqlite_snapshot.add_entity(entity);
3165 }
3166 let snapshot = Snapshot::Sqlite(sqlite_snapshot);
3167
3168 Ok(IntrospectResult {
3169 schema_code: generated.code,
3170 table_count: generated.tables.len(),
3171 index_count: generated.indexes.len(),
3172 view_count: ddl.views.len(),
3173 warnings: generated.warnings,
3174 snapshot,
3175 snapshot_path: std::path::PathBuf::new(),
3176 })
3177}
3178
3179#[cfg(feature = "libsql")]
3184fn introspect_libsql_local(path: &str) -> Result<IntrospectResult, CliError> {
3185 let rt = tokio::runtime::Builder::new_current_thread()
3186 .enable_all()
3187 .build()
3188 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
3189
3190 rt.block_on(introspect_libsql_inner(path, None))
3191}
3192
3193#[cfg(any(feature = "libsql", feature = "turso"))]
3194async fn query_libsql_tables_and_columns(
3195 conn: &libsql::Connection,
3196) -> Result<
3197 (
3198 Vec<(String, Option<String>)>,
3199 Vec<drizzle_migrations::sqlite::introspect::RawColumnInfo>,
3200 ),
3201 CliError,
3202> {
3203 use drizzle_migrations::sqlite::introspect::{RawColumnInfo, queries};
3204
3205 let mut tables_rows = conn
3206 .query(queries::TABLES_QUERY, ())
3207 .await
3208 .map_err(|e| CliError::Other(format!("Failed to query tables: {e}")))?;
3209
3210 let mut tables: Vec<(String, Option<String>)> = Vec::new();
3211 while let Some(row) = tables_rows
3212 .next()
3213 .await
3214 .map_err(|e| CliError::Other(e.to_string()))?
3215 {
3216 let name: String = row.get(0).map_err(|e| CliError::Other(e.to_string()))?;
3217 let sql: Option<String> = row.get(1).ok();
3218 tables.push((name, sql));
3219 }
3220
3221 let mut columns_rows = conn
3222 .query(queries::COLUMNS_QUERY, ())
3223 .await
3224 .map_err(|e| CliError::Other(format!("Failed to query columns: {e}")))?;
3225
3226 let mut raw_columns: Vec<RawColumnInfo> = Vec::new();
3227 while let Some(row) = columns_rows
3228 .next()
3229 .await
3230 .map_err(|e| CliError::Other(e.to_string()))?
3231 {
3232 raw_columns.push(RawColumnInfo {
3233 table: row.get(0).map_err(|e| CliError::Other(e.to_string()))?,
3234 cid: row.get(1).map_err(|e| CliError::Other(e.to_string()))?,
3235 name: row.get(2).map_err(|e| CliError::Other(e.to_string()))?,
3236 column_type: row.get(3).map_err(|e| CliError::Other(e.to_string()))?,
3237 not_null: row
3238 .get::<i32>(4)
3239 .map_err(|e| CliError::Other(e.to_string()))?
3240 != 0,
3241 default_value: row.get(5).ok(),
3242 pk: row.get(6).map_err(|e| CliError::Other(e.to_string()))?,
3243 hidden: row.get(7).map_err(|e| CliError::Other(e.to_string()))?,
3244 sql: row.get(8).ok(),
3245 });
3246 }
3247
3248 Ok((tables, raw_columns))
3249}
3250
3251#[cfg(any(feature = "libsql", feature = "turso"))]
3252async fn query_libsql_indexes_and_foreign_keys(
3253 conn: &libsql::Connection,
3254) -> Result<
3255 (
3256 Vec<drizzle_migrations::sqlite::introspect::RawIndexInfo>,
3257 Vec<drizzle_migrations::sqlite::introspect::RawIndexColumn>,
3258 Vec<drizzle_migrations::sqlite::introspect::RawForeignKey>,
3259 ),
3260 CliError,
3261> {
3262 use drizzle_migrations::sqlite::introspect::{
3263 RawForeignKey, RawIndexColumn, RawIndexInfo, queries,
3264 };
3265
3266 let mut all_indexes = Vec::<RawIndexInfo>::new();
3267 let mut index_rows = conn
3268 .query(queries::INDEXES_QUERY, ())
3269 .await
3270 .map_err(|e| CliError::Other(e.to_string()))?;
3271 while let Some(row) = index_rows
3272 .next()
3273 .await
3274 .map_err(|e| CliError::Other(e.to_string()))?
3275 {
3276 all_indexes.push(RawIndexInfo {
3277 table: row.get(0).map_err(|e| CliError::Other(e.to_string()))?,
3278 name: row.get(1).map_err(|e| CliError::Other(e.to_string()))?,
3279 unique: row
3280 .get::<i32>(2)
3281 .map_err(|e| CliError::Other(e.to_string()))?
3282 != 0,
3283 origin: row.get(3).map_err(|e| CliError::Other(e.to_string()))?,
3284 partial: row
3285 .get::<i32>(4)
3286 .map_err(|e| CliError::Other(e.to_string()))?
3287 != 0,
3288 });
3289 }
3290
3291 let mut all_index_columns = Vec::<RawIndexColumn>::new();
3292 let mut column_rows = conn
3293 .query(queries::INDEX_COLUMNS_QUERY, ())
3294 .await
3295 .map_err(|e| CliError::Other(e.to_string()))?;
3296 while let Some(row) = column_rows
3297 .next()
3298 .await
3299 .map_err(|e| CliError::Other(e.to_string()))?
3300 {
3301 all_index_columns.push(RawIndexColumn {
3302 index_name: row.get(0).map_err(|e| CliError::Other(e.to_string()))?,
3303 seqno: row.get(1).map_err(|e| CliError::Other(e.to_string()))?,
3304 cid: row.get(2).map_err(|e| CliError::Other(e.to_string()))?,
3305 name: row.get(3).ok(),
3306 desc: row
3307 .get::<i32>(4)
3308 .map_err(|e| CliError::Other(e.to_string()))?
3309 != 0,
3310 coll: row.get(5).map_err(|e| CliError::Other(e.to_string()))?,
3311 key: row
3312 .get::<i32>(6)
3313 .map_err(|e| CliError::Other(e.to_string()))?
3314 != 0,
3315 });
3316 }
3317
3318 let mut all_fks = Vec::<RawForeignKey>::new();
3319 let mut foreign_key_rows = conn
3320 .query(queries::FOREIGN_KEYS_QUERY, ())
3321 .await
3322 .map_err(|e| CliError::Other(e.to_string()))?;
3323 while let Some(row) = foreign_key_rows
3324 .next()
3325 .await
3326 .map_err(|e| CliError::Other(e.to_string()))?
3327 {
3328 all_fks.push(RawForeignKey {
3329 table: row.get(0).map_err(|e| CliError::Other(e.to_string()))?,
3330 id: row.get(1).map_err(|e| CliError::Other(e.to_string()))?,
3331 seq: row.get(2).map_err(|e| CliError::Other(e.to_string()))?,
3332 to_table: row.get(3).map_err(|e| CliError::Other(e.to_string()))?,
3333 from_column: row.get(4).map_err(|e| CliError::Other(e.to_string()))?,
3334 to_column: row.get(5).map_err(|e| CliError::Other(e.to_string()))?,
3335 on_update: row.get(6).map_err(|e| CliError::Other(e.to_string()))?,
3336 on_delete: row.get(7).map_err(|e| CliError::Other(e.to_string()))?,
3337 r#match: row.get(8).map_err(|e| CliError::Other(e.to_string()))?,
3338 });
3339 }
3340
3341 Ok((all_indexes, all_index_columns, all_fks))
3342}
3343
3344#[cfg(any(feature = "libsql", feature = "turso"))]
3345async fn query_libsql_views_and_view_columns(
3346 conn: &libsql::Connection,
3347 raw_columns: &mut Vec<drizzle_migrations::sqlite::introspect::RawColumnInfo>,
3348) -> Result<Vec<drizzle_migrations::sqlite::introspect::RawViewInfo>, CliError> {
3349 use drizzle_migrations::sqlite::introspect::{RawColumnInfo, RawViewInfo, queries};
3350
3351 let mut all_views: Vec<RawViewInfo> = Vec::new();
3352 let mut views_rows = conn
3353 .query(queries::VIEWS_QUERY, ())
3354 .await
3355 .map_err(|e| CliError::Other(e.to_string()))?;
3356 while let Some(row) = views_rows
3357 .next()
3358 .await
3359 .map_err(|e| CliError::Other(e.to_string()))?
3360 {
3361 let name: String = row.get(0).map_err(|e| CliError::Other(e.to_string()))?;
3362 let sql: String = row.get(1).map_err(|e| CliError::Other(e.to_string()))?;
3363 all_views.push(RawViewInfo { name, sql });
3364 }
3365
3366 let mut view_cols_rows = conn
3367 .query(queries::VIEW_COLUMNS_QUERY, ())
3368 .await
3369 .map_err(|e| CliError::Other(e.to_string()))?;
3370 while let Some(row) = view_cols_rows
3371 .next()
3372 .await
3373 .map_err(|e| CliError::Other(e.to_string()))?
3374 {
3375 raw_columns.push(RawColumnInfo {
3376 table: row.get(0).map_err(|e| CliError::Other(e.to_string()))?,
3377 cid: row.get(1).map_err(|e| CliError::Other(e.to_string()))?,
3378 name: row.get(2).map_err(|e| CliError::Other(e.to_string()))?,
3379 column_type: row.get(3).map_err(|e| CliError::Other(e.to_string()))?,
3380 not_null: row
3381 .get::<i32>(4)
3382 .map_err(|e| CliError::Other(e.to_string()))?
3383 != 0,
3384 default_value: row.get(5).ok(),
3385 pk: row.get(6).map_err(|e| CliError::Other(e.to_string()))?,
3386 hidden: row.get(7).map_err(|e| CliError::Other(e.to_string()))?,
3387 sql: row.get(8).ok(),
3388 });
3389 }
3390 Ok(all_views)
3391}
3392
3393#[cfg(any(feature = "libsql", feature = "turso"))]
3394async fn query_libsql_raw(conn: &libsql::Connection) -> Result<SqliteRawData, CliError> {
3395 let (tables, mut raw_columns) = query_libsql_tables_and_columns(conn).await?;
3396 let (all_indexes, all_index_columns, all_fks) =
3397 query_libsql_indexes_and_foreign_keys(conn).await?;
3398 let all_views = query_libsql_views_and_view_columns(conn, &mut raw_columns).await?;
3399
3400 Ok(SqliteRawData {
3401 tables,
3402 raw_columns,
3403 all_indexes,
3404 all_index_columns,
3405 all_fks,
3406 all_views,
3407 })
3408}
3409
3410#[cfg(feature = "libsql")]
3411async fn introspect_libsql_inner(
3412 path: &str,
3413 _auth_token: Option<&str>,
3414) -> Result<IntrospectResult, CliError> {
3415 use drizzle_migrations::sqlite::codegen::{CodegenOptions, FieldCasing, generate_rust_schema};
3416
3417 let db = libsql::Builder::new_local(path)
3418 .build()
3419 .await
3420 .map_err(|e| {
3421 CliError::ConnectionError(format!("Failed to open LibSQL database '{path}': {e}"))
3422 })?;
3423
3424 let conn = db
3425 .connect()
3426 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
3427
3428 let raw = query_libsql_raw(&conn).await?;
3429 let ddl = build_sqlite_ddl(raw);
3430
3431 let options = CodegenOptions {
3432 module_doc: Some(format!("Schema introspected from {path}")),
3433 include_schema: true,
3434 schema_name: "Schema".to_string(),
3435 use_pub: true,
3436 field_casing: FieldCasing::default(),
3437 };
3438
3439 let generated = generate_rust_schema(&ddl, &options);
3440
3441 let mut sqlite_snapshot = drizzle_migrations::sqlite::SQLiteSnapshot::new();
3442 for entity in ddl.to_entities() {
3443 sqlite_snapshot.add_entity(entity);
3444 }
3445 let snapshot = Snapshot::Sqlite(sqlite_snapshot);
3446
3447 Ok(IntrospectResult {
3448 schema_code: generated.code,
3449 table_count: generated.tables.len(),
3450 index_count: generated.indexes.len(),
3451 view_count: ddl.views.len(),
3452 warnings: generated.warnings,
3453 snapshot,
3454 snapshot_path: std::path::PathBuf::new(),
3455 })
3456}
3457
3458#[cfg(feature = "turso")]
3463fn introspect_turso(url: &str, auth_token: Option<&str>) -> Result<IntrospectResult, CliError> {
3464 let rt = tokio::runtime::Builder::new_current_thread()
3465 .enable_all()
3466 .build()
3467 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
3468
3469 rt.block_on(introspect_turso_inner(url, auth_token))
3470}
3471
3472#[cfg(feature = "turso")]
3473async fn introspect_turso_inner(
3474 url: &str,
3475 auth_token: Option<&str>,
3476) -> Result<IntrospectResult, CliError> {
3477 use drizzle_migrations::sqlite::codegen::{CodegenOptions, FieldCasing, generate_rust_schema};
3478
3479 let builder =
3480 libsql::Builder::new_remote(url.to_string(), auth_token.unwrap_or("").to_string());
3481
3482 let db = builder.build().await.map_err(|e| {
3483 CliError::ConnectionError(format!("Failed to connect to Turso '{url}': {e}"))
3484 })?;
3485
3486 let conn = db
3487 .connect()
3488 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
3489
3490 let raw = query_libsql_raw(&conn).await?;
3491 let ddl = build_sqlite_ddl(raw);
3492
3493 let options = CodegenOptions {
3494 module_doc: Some(format!("Schema introspected from Turso: {url}")),
3495 include_schema: true,
3496 schema_name: "Schema".to_string(),
3497 use_pub: true,
3498 field_casing: FieldCasing::default(),
3499 };
3500
3501 let generated = generate_rust_schema(&ddl, &options);
3502
3503 let mut sqlite_snapshot = drizzle_migrations::sqlite::SQLiteSnapshot::new();
3504 for entity in ddl.to_entities() {
3505 sqlite_snapshot.add_entity(entity);
3506 }
3507 let snapshot = Snapshot::Sqlite(sqlite_snapshot);
3508
3509 Ok(IntrospectResult {
3510 schema_code: generated.code,
3511 table_count: generated.tables.len(),
3512 index_count: generated.indexes.len(),
3513 view_count: ddl.views.len(),
3514 warnings: generated.warnings,
3515 snapshot,
3516 snapshot_path: std::path::PathBuf::new(),
3517 })
3518}
3519
3520#[cfg(feature = "postgres-sync")]
3525fn introspect_postgres_sync(creds: &PostgresCreds) -> Result<IntrospectResult, CliError> {
3526 let mut client = connect_postgres_sync(creds)?;
3527
3528 let raw = query_postgres_sync_raw(&mut client)?;
3529 let ddl = build_postgres_ddl(raw);
3530
3531 Ok(finalize_postgres_introspection(
3532 &ddl,
3533 "configured PostgreSQL database",
3534 ))
3535}
3536
3537#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
3538impl PostgresRawData {
3539 const fn empty() -> Self {
3540 Self {
3541 schemas: Vec::new(),
3542 tables: Vec::new(),
3543 columns: Vec::new(),
3544 enums: Vec::new(),
3545 sequences: Vec::new(),
3546 views: Vec::new(),
3547 indexes: Vec::new(),
3548 foreign_keys: Vec::new(),
3549 primary_keys: Vec::new(),
3550 uniques: Vec::new(),
3551 checks: Vec::new(),
3552 roles: Vec::new(),
3553 policies: Vec::new(),
3554 }
3555 }
3556}
3557
3558#[cfg(feature = "postgres-sync")]
3559fn query_postgres_sync_raw(client: &mut postgres::Client) -> Result<PostgresRawData, CliError> {
3560 let mut raw = PostgresRawData::empty();
3561 query_pg_sync_core(client, &mut raw)?;
3562 query_pg_sync_codegen_meta(client, &mut raw)?;
3563 query_pg_sync_constraints(client, &mut raw)?;
3564 query_pg_sync_security(client, &mut raw)?;
3565 Ok(raw)
3566}
3567
3568#[cfg(feature = "postgres-sync")]
3569fn query_pg_sync_core(
3570 client: &mut postgres::Client,
3571 raw: &mut PostgresRawData,
3572) -> Result<(), CliError> {
3573 use drizzle_migrations::postgres::introspect::{RawColumnInfo, RawTableInfo, queries};
3574
3575 raw.schemas = client
3576 .query(queries::SCHEMAS_QUERY, &[])
3577 .map_err(|e| CliError::Other(format!("Failed to query schemas: {e}")))?
3578 .into_iter()
3579 .map(|row| RawSchemaInfo {
3580 name: row.get::<_, String>(0),
3581 })
3582 .collect();
3583
3584 raw.tables = client
3585 .query(queries::TABLES_QUERY, &[])
3586 .map_err(|e| CliError::Other(format!("Failed to query tables: {e}")))?
3587 .into_iter()
3588 .map(|row| RawTableInfo {
3589 schema: row.get::<_, String>(0),
3590 name: row.get::<_, String>(1),
3591 is_rls_enabled: row.get::<_, bool>(2),
3592 is_unlogged: row.get::<_, bool>(3),
3593 is_temporary: row.get::<_, bool>(4),
3594 tablespace: row.get::<_, Option<String>>(5),
3595 comment: row.get::<_, Option<String>>(6),
3596 })
3597 .collect();
3598
3599 raw.columns = client
3600 .query(queries::COLUMNS_QUERY, &[])
3601 .map_err(|e| CliError::Other(format!("Failed to query columns: {e}")))?
3602 .into_iter()
3603 .map(|row| RawColumnInfo {
3604 schema: row.get::<_, String>(0),
3605 table: row.get::<_, String>(1),
3606 name: row.get::<_, String>(2),
3607 column_type: row.get::<_, String>(3),
3608 type_schema: row.get::<_, Option<String>>(4),
3609 not_null: row.get::<_, bool>(5),
3610 default_value: row.get::<_, Option<String>>(6),
3611 is_identity: row.get::<_, bool>(7),
3612 identity_type: row.get::<_, Option<String>>(8),
3613 is_generated: row.get::<_, bool>(9),
3614 generated_expression: row.get::<_, Option<String>>(10),
3615 generated_stored: row.get::<_, bool>(11),
3616 dimensions: row.get::<_, Option<i32>>(12),
3617 comment: row.get::<_, Option<String>>(13),
3618 ordinal_position: row.get::<_, i32>(14),
3619 })
3620 .collect();
3621
3622 Ok(())
3623}
3624
3625#[cfg(feature = "postgres-sync")]
3626fn query_pg_sync_codegen_meta(
3627 client: &mut postgres::Client,
3628 raw: &mut PostgresRawData,
3629) -> Result<(), CliError> {
3630 use drizzle_migrations::postgres::introspect::{
3631 RawEnumInfo, RawSequenceInfo, RawViewInfo, queries,
3632 };
3633
3634 raw.enums = client
3635 .query(queries::ENUMS_QUERY, &[])
3636 .map_err(|e| CliError::Other(format!("Failed to query enums: {e}")))?
3637 .into_iter()
3638 .map(|row| RawEnumInfo {
3639 schema: row.get::<_, String>(0),
3640 name: row.get::<_, String>(1),
3641 values: row.get::<_, Vec<String>>(2),
3642 })
3643 .collect();
3644
3645 raw.sequences = client
3646 .query(queries::SEQUENCES_QUERY, &[])
3647 .map_err(|e| CliError::Other(format!("Failed to query sequences: {e}")))?
3648 .into_iter()
3649 .map(|row| RawSequenceInfo {
3650 schema: row.get::<_, String>(0),
3651 name: row.get::<_, String>(1),
3652 data_type: row.get::<_, Option<String>>(2),
3653 start_value: row.get::<_, Option<String>>(3),
3654 min_value: row.get::<_, Option<String>>(4),
3655 max_value: row.get::<_, Option<String>>(5),
3656 increment: row.get::<_, Option<String>>(6),
3657 cycle: row.get::<_, Option<bool>>(7),
3658 cache_value: row.get::<_, Option<String>>(8),
3659 })
3660 .collect();
3661
3662 let view_schema_filters: Option<Vec<String>> = None;
3663 raw.views = client
3664 .query(queries::VIEWS_QUERY, &[&view_schema_filters])
3665 .map_err(|e| CliError::Other(format!("Failed to query views: {e}")))?
3666 .into_iter()
3667 .map(|row| RawViewInfo {
3668 schema: row.get::<_, String>(0),
3669 name: row.get::<_, String>(1),
3670 definition: row.get::<_, String>(2),
3671 is_materialized: row.get::<_, bool>(3),
3672 })
3673 .collect();
3674
3675 Ok(())
3676}
3677
3678#[cfg(feature = "postgres-sync")]
3679fn query_pg_sync_constraints(
3680 client: &mut postgres::Client,
3681 raw: &mut PostgresRawData,
3682) -> Result<(), CliError> {
3683 use drizzle_migrations::postgres::introspect::{
3684 RawCheckInfo, RawForeignKeyInfo, RawIndexInfo, RawPrimaryKeyInfo, RawUniqueInfo,
3685 parse_index_columns, pg_action_code_to_string, queries,
3686 };
3687
3688 raw.indexes = client
3689 .query(queries::INDEXES_QUERY, &[])
3690 .map_err(|e| CliError::Other(format!("Failed to query indexes: {e}")))?
3691 .into_iter()
3692 .map(|row| {
3693 let cols: Vec<String> = row.get(6);
3694 RawIndexInfo {
3695 schema: row.get::<_, String>(0),
3696 table: row.get::<_, String>(1),
3697 name: row.get::<_, String>(2),
3698 is_unique: row.get::<_, bool>(3),
3699 is_primary: row.get::<_, bool>(4),
3700 method: row.get::<_, String>(5),
3701 columns: parse_index_columns(cols),
3702 where_clause: row.get::<_, Option<String>>(7),
3703 concurrent: false,
3704 }
3705 })
3706 .collect();
3707
3708 raw.foreign_keys = client
3709 .query(queries::FOREIGN_KEYS_QUERY, &[])
3710 .map_err(|e| CliError::Other(format!("Failed to query foreign keys: {e}")))?
3711 .into_iter()
3712 .map(|row| RawForeignKeyInfo {
3713 schema: row.get::<_, String>(0),
3714 table: row.get::<_, String>(1),
3715 name: row.get::<_, String>(2),
3716 columns: row.get::<_, Vec<String>>(3),
3717 schema_to: row.get::<_, String>(4),
3718 table_to: row.get::<_, String>(5),
3719 columns_to: row.get::<_, Vec<String>>(6),
3720 on_update: pg_action_code_to_string(&row.get::<_, String>(7)),
3721 on_delete: pg_action_code_to_string(&row.get::<_, String>(8)),
3722 deferrable: row.get::<_, bool>(9),
3723 initially_deferred: row.get::<_, bool>(10),
3724 })
3725 .collect();
3726
3727 raw.primary_keys = client
3728 .query(queries::PRIMARY_KEYS_QUERY, &[])
3729 .map_err(|e| CliError::Other(format!("Failed to query primary keys: {e}")))?
3730 .into_iter()
3731 .map(|row| RawPrimaryKeyInfo {
3732 schema: row.get::<_, String>(0),
3733 table: row.get::<_, String>(1),
3734 name: row.get::<_, String>(2),
3735 columns: row.get::<_, Vec<String>>(3),
3736 })
3737 .collect();
3738
3739 raw.uniques = client
3740 .query(queries::UNIQUES_QUERY, &[])
3741 .map_err(|e| CliError::Other(format!("Failed to query unique constraints: {e}")))?
3742 .into_iter()
3743 .map(|row| RawUniqueInfo {
3744 schema: row.get::<_, String>(0),
3745 table: row.get::<_, String>(1),
3746 name: row.get::<_, String>(2),
3747 columns: row.get::<_, Vec<String>>(3),
3748 nulls_not_distinct: row.get::<_, bool>(4),
3749 deferrable: row.get::<_, bool>(5),
3750 initially_deferred: row.get::<_, bool>(6),
3751 })
3752 .collect();
3753
3754 raw.checks = client
3755 .query(queries::CHECKS_QUERY, &[])
3756 .map_err(|e| CliError::Other(format!("Failed to query check constraints: {e}")))?
3757 .into_iter()
3758 .map(|row| RawCheckInfo {
3759 schema: row.get::<_, String>(0),
3760 table: row.get::<_, String>(1),
3761 name: row.get::<_, String>(2),
3762 expression: row.get::<_, String>(3),
3763 })
3764 .collect();
3765
3766 Ok(())
3767}
3768
3769#[cfg(feature = "postgres-sync")]
3770fn query_pg_sync_security(
3771 client: &mut postgres::Client,
3772 raw: &mut PostgresRawData,
3773) -> Result<(), CliError> {
3774 use drizzle_migrations::postgres::introspect::{RawPolicyInfo, RawRoleInfo, queries};
3775
3776 raw.roles = client
3777 .query(queries::ROLES_QUERY, &[])
3778 .map_err(|e| CliError::Other(format!("Failed to query roles: {e}")))?
3779 .into_iter()
3780 .map(|row| RawRoleInfo {
3781 name: row.get::<_, String>(0),
3782 create_db: row.get::<_, bool>(1),
3783 create_role: row.get::<_, bool>(2),
3784 inherit: row.get::<_, bool>(3),
3785 })
3786 .collect();
3787
3788 raw.policies = client
3789 .query(queries::POLICIES_QUERY, &[])
3790 .map_err(|e| CliError::Other(format!("Failed to query policies: {e}")))?
3791 .into_iter()
3792 .map(|row| RawPolicyInfo {
3793 schema: row.get::<_, String>(0),
3794 table: row.get::<_, String>(1),
3795 name: row.get::<_, String>(2),
3796 as_clause: row.get::<_, String>(3),
3797 for_clause: row.get::<_, String>(4),
3798 to: row.get::<_, Vec<String>>(5),
3799 using: row.get::<_, Option<String>>(6),
3800 with_check: row.get::<_, Option<String>>(7),
3801 })
3802 .collect();
3803
3804 Ok(())
3805}
3806
3807#[cfg(all(not(feature = "postgres-sync"), feature = "tokio-postgres"))]
3808fn introspect_postgres_async(creds: &PostgresCreds) -> Result<IntrospectResult, CliError> {
3809 let rt = tokio::runtime::Builder::new_current_thread()
3810 .enable_all()
3811 .build()
3812 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
3813
3814 rt.block_on(introspect_postgres_async_inner(creds))
3815}
3816
3817#[cfg(all(not(feature = "postgres-sync"), feature = "tokio-postgres"))]
3818async fn introspect_postgres_async_inner(
3819 creds: &PostgresCreds,
3820) -> Result<IntrospectResult, CliError> {
3821 let client = connect_postgres_async(creds).await?;
3822
3823 let raw = query_postgres_async_raw(&client).await?;
3824 let ddl = build_postgres_ddl(raw);
3825
3826 Ok(finalize_postgres_introspection(
3827 &ddl,
3828 "configured PostgreSQL database",
3829 ))
3830}
3831
3832#[cfg(all(not(feature = "postgres-sync"), feature = "tokio-postgres"))]
3833async fn query_postgres_async_raw(
3834 client: &tokio_postgres::Client,
3835) -> Result<PostgresRawData, CliError> {
3836 let mut raw = PostgresRawData::empty();
3837 query_pg_async_core(client, &mut raw).await?;
3838 query_pg_async_codegen_meta(client, &mut raw).await?;
3839 query_pg_async_constraints(client, &mut raw).await?;
3840 query_pg_async_security(client, &mut raw).await?;
3841 Ok(raw)
3842}
3843
3844#[cfg(all(not(feature = "postgres-sync"), feature = "tokio-postgres"))]
3845async fn query_pg_async_core(
3846 client: &tokio_postgres::Client,
3847 raw: &mut PostgresRawData,
3848) -> Result<(), CliError> {
3849 use drizzle_migrations::postgres::introspect::{RawColumnInfo, RawTableInfo, queries};
3850
3851 raw.schemas = client
3852 .query(queries::SCHEMAS_QUERY, &[])
3853 .await
3854 .map_err(|e| CliError::Other(format!("Failed to query schemas: {e}")))?
3855 .into_iter()
3856 .map(|row| RawSchemaInfo {
3857 name: row.get::<_, String>(0),
3858 })
3859 .collect();
3860
3861 raw.tables = client
3862 .query(queries::TABLES_QUERY, &[])
3863 .await
3864 .map_err(|e| CliError::Other(format!("Failed to query tables: {e}")))?
3865 .into_iter()
3866 .map(|row| RawTableInfo {
3867 schema: row.get::<_, String>(0),
3868 name: row.get::<_, String>(1),
3869 is_rls_enabled: row.get::<_, bool>(2),
3870 is_unlogged: row.get::<_, bool>(3),
3871 is_temporary: row.get::<_, bool>(4),
3872 tablespace: row.get::<_, Option<String>>(5),
3873 comment: row.get::<_, Option<String>>(6),
3874 })
3875 .collect();
3876
3877 raw.columns = client
3878 .query(queries::COLUMNS_QUERY, &[])
3879 .await
3880 .map_err(|e| CliError::Other(format!("Failed to query columns: {e}")))?
3881 .into_iter()
3882 .map(|row| RawColumnInfo {
3883 schema: row.get::<_, String>(0),
3884 table: row.get::<_, String>(1),
3885 name: row.get::<_, String>(2),
3886 column_type: row.get::<_, String>(3),
3887 type_schema: row.get::<_, Option<String>>(4),
3888 not_null: row.get::<_, bool>(5),
3889 default_value: row.get::<_, Option<String>>(6),
3890 is_identity: row.get::<_, bool>(7),
3891 identity_type: row.get::<_, Option<String>>(8),
3892 is_generated: row.get::<_, bool>(9),
3893 generated_expression: row.get::<_, Option<String>>(10),
3894 generated_stored: row.get::<_, bool>(11),
3895 dimensions: row.get::<_, Option<i32>>(12),
3896 comment: row.get::<_, Option<String>>(13),
3897 ordinal_position: row.get::<_, i32>(14),
3898 })
3899 .collect();
3900
3901 Ok(())
3902}
3903
3904#[cfg(all(not(feature = "postgres-sync"), feature = "tokio-postgres"))]
3905async fn query_pg_async_codegen_meta(
3906 client: &tokio_postgres::Client,
3907 raw: &mut PostgresRawData,
3908) -> Result<(), CliError> {
3909 use drizzle_migrations::postgres::introspect::{
3910 RawEnumInfo, RawSequenceInfo, RawViewInfo, queries,
3911 };
3912
3913 raw.enums = client
3914 .query(queries::ENUMS_QUERY, &[])
3915 .await
3916 .map_err(|e| CliError::Other(format!("Failed to query enums: {e}")))?
3917 .into_iter()
3918 .map(|row| RawEnumInfo {
3919 schema: row.get::<_, String>(0),
3920 name: row.get::<_, String>(1),
3921 values: row.get::<_, Vec<String>>(2),
3922 })
3923 .collect();
3924
3925 raw.sequences = client
3926 .query(queries::SEQUENCES_QUERY, &[])
3927 .await
3928 .map_err(|e| CliError::Other(format!("Failed to query sequences: {e}")))?
3929 .into_iter()
3930 .map(|row| RawSequenceInfo {
3931 schema: row.get::<_, String>(0),
3932 name: row.get::<_, String>(1),
3933 data_type: row.get::<_, Option<String>>(2),
3934 start_value: row.get::<_, Option<String>>(3),
3935 min_value: row.get::<_, Option<String>>(4),
3936 max_value: row.get::<_, Option<String>>(5),
3937 increment: row.get::<_, Option<String>>(6),
3938 cycle: row.get::<_, Option<bool>>(7),
3939 cache_value: row.get::<_, Option<String>>(8),
3940 })
3941 .collect();
3942
3943 let view_schema_filters: Option<Vec<String>> = None;
3944 raw.views = client
3945 .query(queries::VIEWS_QUERY, &[&view_schema_filters])
3946 .await
3947 .map_err(|e| CliError::Other(format!("Failed to query views: {e}")))?
3948 .into_iter()
3949 .map(|row| RawViewInfo {
3950 schema: row.get::<_, String>(0),
3951 name: row.get::<_, String>(1),
3952 definition: row.get::<_, String>(2),
3953 is_materialized: row.get::<_, bool>(3),
3954 })
3955 .collect();
3956
3957 Ok(())
3958}
3959
3960#[cfg(all(not(feature = "postgres-sync"), feature = "tokio-postgres"))]
3961async fn query_pg_async_constraints(
3962 client: &tokio_postgres::Client,
3963 raw: &mut PostgresRawData,
3964) -> Result<(), CliError> {
3965 use drizzle_migrations::postgres::introspect::{
3966 RawCheckInfo, RawForeignKeyInfo, RawIndexInfo, RawPrimaryKeyInfo, RawUniqueInfo,
3967 parse_index_columns, pg_action_code_to_string, queries,
3968 };
3969
3970 raw.indexes = client
3971 .query(queries::INDEXES_QUERY, &[])
3972 .await
3973 .map_err(|e| CliError::Other(format!("Failed to query indexes: {e}")))?
3974 .into_iter()
3975 .map(|row| {
3976 let cols: Vec<String> = row.get(6);
3977 RawIndexInfo {
3978 schema: row.get::<_, String>(0),
3979 table: row.get::<_, String>(1),
3980 name: row.get::<_, String>(2),
3981 is_unique: row.get::<_, bool>(3),
3982 is_primary: row.get::<_, bool>(4),
3983 method: row.get::<_, String>(5),
3984 columns: parse_index_columns(cols),
3985 where_clause: row.get::<_, Option<String>>(7),
3986 concurrent: false,
3987 }
3988 })
3989 .collect();
3990
3991 raw.foreign_keys = client
3992 .query(queries::FOREIGN_KEYS_QUERY, &[])
3993 .await
3994 .map_err(|e| CliError::Other(format!("Failed to query foreign keys: {e}")))?
3995 .into_iter()
3996 .map(|row| RawForeignKeyInfo {
3997 schema: row.get::<_, String>(0),
3998 table: row.get::<_, String>(1),
3999 name: row.get::<_, String>(2),
4000 columns: row.get::<_, Vec<String>>(3),
4001 schema_to: row.get::<_, String>(4),
4002 table_to: row.get::<_, String>(5),
4003 columns_to: row.get::<_, Vec<String>>(6),
4004 on_update: pg_action_code_to_string(&row.get::<_, String>(7)),
4005 on_delete: pg_action_code_to_string(&row.get::<_, String>(8)),
4006 deferrable: row.get::<_, bool>(9),
4007 initially_deferred: row.get::<_, bool>(10),
4008 })
4009 .collect();
4010
4011 raw.primary_keys = client
4012 .query(queries::PRIMARY_KEYS_QUERY, &[])
4013 .await
4014 .map_err(|e| CliError::Other(format!("Failed to query primary keys: {e}")))?
4015 .into_iter()
4016 .map(|row| RawPrimaryKeyInfo {
4017 schema: row.get::<_, String>(0),
4018 table: row.get::<_, String>(1),
4019 name: row.get::<_, String>(2),
4020 columns: row.get::<_, Vec<String>>(3),
4021 })
4022 .collect();
4023
4024 raw.uniques = client
4025 .query(queries::UNIQUES_QUERY, &[])
4026 .await
4027 .map_err(|e| CliError::Other(format!("Failed to query unique constraints: {e}")))?
4028 .into_iter()
4029 .map(|row| RawUniqueInfo {
4030 schema: row.get::<_, String>(0),
4031 table: row.get::<_, String>(1),
4032 name: row.get::<_, String>(2),
4033 columns: row.get::<_, Vec<String>>(3),
4034 nulls_not_distinct: row.get::<_, bool>(4),
4035 deferrable: row.get::<_, bool>(5),
4036 initially_deferred: row.get::<_, bool>(6),
4037 })
4038 .collect();
4039
4040 raw.checks = client
4041 .query(queries::CHECKS_QUERY, &[])
4042 .await
4043 .map_err(|e| CliError::Other(format!("Failed to query check constraints: {e}")))?
4044 .into_iter()
4045 .map(|row| RawCheckInfo {
4046 schema: row.get::<_, String>(0),
4047 table: row.get::<_, String>(1),
4048 name: row.get::<_, String>(2),
4049 expression: row.get::<_, String>(3),
4050 })
4051 .collect();
4052
4053 Ok(())
4054}
4055
4056#[cfg(all(not(feature = "postgres-sync"), feature = "tokio-postgres"))]
4057async fn query_pg_async_security(
4058 client: &tokio_postgres::Client,
4059 raw: &mut PostgresRawData,
4060) -> Result<(), CliError> {
4061 use drizzle_migrations::postgres::introspect::{RawPolicyInfo, RawRoleInfo, queries};
4062
4063 raw.roles = client
4064 .query(queries::ROLES_QUERY, &[])
4065 .await
4066 .map_err(|e| CliError::Other(format!("Failed to query roles: {e}")))?
4067 .into_iter()
4068 .map(|row| RawRoleInfo {
4069 name: row.get::<_, String>(0),
4070 create_db: row.get::<_, bool>(1),
4071 create_role: row.get::<_, bool>(2),
4072 inherit: row.get::<_, bool>(3),
4073 })
4074 .collect();
4075
4076 raw.policies = client
4077 .query(queries::POLICIES_QUERY, &[])
4078 .await
4079 .map_err(|e| CliError::Other(format!("Failed to query policies: {e}")))?
4080 .into_iter()
4081 .map(|row| RawPolicyInfo {
4082 schema: row.get::<_, String>(0),
4083 table: row.get::<_, String>(1),
4084 name: row.get::<_, String>(2),
4085 as_clause: row.get::<_, String>(3),
4086 for_clause: row.get::<_, String>(4),
4087 to: row.get::<_, Vec<String>>(5),
4088 using: row.get::<_, Option<String>>(6),
4089 with_check: row.get::<_, Option<String>>(7),
4090 })
4091 .collect();
4092
4093 Ok(())
4094}
4095
4096#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
4098#[derive(Debug, Clone)]
4099struct RawSchemaInfo {
4100 name: String,
4101}
4102
4103#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
4109struct PostgresRawData {
4110 schemas: Vec<RawSchemaInfo>,
4111 tables: Vec<drizzle_migrations::postgres::introspect::RawTableInfo>,
4112 columns: Vec<drizzle_migrations::postgres::introspect::RawColumnInfo>,
4113 enums: Vec<drizzle_migrations::postgres::introspect::RawEnumInfo>,
4114 sequences: Vec<drizzle_migrations::postgres::introspect::RawSequenceInfo>,
4115 views: Vec<drizzle_migrations::postgres::introspect::RawViewInfo>,
4116 indexes: Vec<drizzle_migrations::postgres::introspect::RawIndexInfo>,
4117 foreign_keys: Vec<drizzle_migrations::postgres::introspect::RawForeignKeyInfo>,
4118 primary_keys: Vec<drizzle_migrations::postgres::introspect::RawPrimaryKeyInfo>,
4119 uniques: Vec<drizzle_migrations::postgres::introspect::RawUniqueInfo>,
4120 checks: Vec<drizzle_migrations::postgres::introspect::RawCheckInfo>,
4121 roles: Vec<drizzle_migrations::postgres::introspect::RawRoleInfo>,
4122 policies: Vec<drizzle_migrations::postgres::introspect::RawPolicyInfo>,
4123}
4124
4125#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
4129fn build_postgres_ddl(
4130 raw: PostgresRawData,
4131) -> drizzle_migrations::postgres::collection::PostgresDDL {
4132 use drizzle_migrations::postgres::ddl::Schema;
4133 use drizzle_migrations::postgres::introspect::{RawIntrospection, assemble_ddl};
4134
4135 assemble_ddl(RawIntrospection {
4136 schemas: raw
4137 .schemas
4138 .into_iter()
4139 .map(|schema| Schema::new(schema.name))
4140 .collect(),
4141 tables: raw.tables,
4142 columns: raw.columns,
4143 enums: raw.enums,
4144 sequences: raw.sequences,
4145 views: raw.views,
4146 indexes: raw.indexes,
4147 foreign_keys: raw.foreign_keys,
4148 primary_keys: raw.primary_keys,
4149 unique_constraints: raw.uniques,
4150 check_constraints: raw.checks,
4151 roles: raw.roles,
4152 policies: raw.policies,
4153 })
4154}
4155
4156#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
4158fn finalize_postgres_introspection(
4159 ddl: &drizzle_migrations::postgres::collection::PostgresDDL,
4160 url: &str,
4161) -> IntrospectResult {
4162 use drizzle_migrations::postgres::codegen::{
4163 CodegenOptions, FieldCasing, generate_rust_schema,
4164 };
4165
4166 let options = CodegenOptions {
4167 module_doc: Some(format!("Schema introspected from {}", mask_url(url))),
4168 include_schema: true,
4169 schema_name: "Schema".to_string(),
4170 use_pub: true,
4171 field_casing: FieldCasing::default(),
4172 };
4173 let generated = generate_rust_schema(ddl, &options);
4174
4175 let mut snap = drizzle_migrations::postgres::PostgresSnapshot::new();
4176 for entity in ddl.to_entities() {
4177 snap.add_entity(entity);
4178 }
4179
4180 IntrospectResult {
4181 schema_code: generated.code,
4182 table_count: ddl.tables.list().len(),
4183 index_count: ddl.indexes.list().len(),
4184 view_count: ddl.views.list().len(),
4185 warnings: generated.warnings,
4186 snapshot: Snapshot::Postgres(snap),
4187 snapshot_path: std::path::PathBuf::new(),
4188 }
4189}
4190
4191#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
4192fn mask_url(url: &str) -> String {
4193 if let Some(at) = url.find('@')
4194 && let Some(colon) = url[..at].rfind(':')
4195 {
4196 let scheme_end = url.find("://").map_or(0, |p| p + 3);
4197 if colon > scheme_end {
4198 return format!("{}****{}", &url[..=colon], &url[at..]);
4199 }
4200 }
4201 url.to_string()
4202}
4203
4204#[cfg(test)]
4205mod tests {
4206 use super::*;
4207
4208 #[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
4209 fn test_postgres_url() -> String {
4210 std::env::var("DATABASE_URL")
4211 .unwrap_or_else(|_| "postgres://postgres:postgres@localhost:5432/drizzle_test".into())
4212 }
4213
4214 #[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
4215 fn test_postgres_creds() -> crate::config::PostgresCreds {
4216 crate::config::PostgresCreds::Url(test_postgres_url().into_boxed_str())
4217 }
4218
4219 #[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
4220 fn unique_pg_name(prefix: &str) -> String {
4221 use std::time::{SystemTime, UNIX_EPOCH};
4222
4223 let nanos = SystemTime::now()
4224 .duration_since(UNIX_EPOCH)
4225 .expect("time went backwards")
4226 .as_nanos();
4227 format!("{}_{}_{}", prefix, std::process::id(), nanos)
4228 }
4229
4230 #[test]
4231 fn destructive_statement_detection_covers_drop_variants() {
4232 assert!(is_destructive_statement("DROP TABLE users;"));
4233 assert!(is_destructive_statement("DROP VIEW active_users;"));
4234 assert!(is_destructive_statement("DROP TYPE status;"));
4235 assert!(is_destructive_statement("DROP SCHEMA auth;"));
4236 assert!(is_destructive_statement("DROP ROLE app_user;"));
4237 assert!(is_destructive_statement(
4238 "DROP POLICY users_rls_policy ON users;"
4239 ));
4240 assert!(is_destructive_statement("TRUNCATE users;"));
4241 assert!(is_destructive_statement(
4242 "ALTER TABLE users DROP CONSTRAINT users_email_key;"
4243 ));
4244
4245 assert!(!is_destructive_statement("CREATE TABLE users(id INTEGER);"));
4246 assert!(!is_destructive_statement(
4247 "ALTER TABLE users ADD COLUMN email text;"
4248 ));
4249 }
4250
4251 #[cfg(feature = "rusqlite")]
4252 #[test]
4253 fn sqlite_migrations_run_both_when_created_at_collides() {
4254 use drizzle_migrations::{Migration, Migrations};
4258
4259 let dir = tempfile::tempdir().expect("tempdir");
4260 let db_path = dir.path().join("migrate.sqlite");
4261 let db_path_str = db_path.to_string_lossy().to_string();
4262
4263 let first_set = Migrations::new(
4264 vec![Migration::with_hash(
4265 "20230331141203_first",
4266 "hash_one",
4267 1_680_271_923_000,
4268 vec!["CREATE TABLE created_at_dedupe_a (id INTEGER PRIMARY KEY)".to_string()],
4269 )],
4270 drizzle_types::Dialect::SQLite,
4271 );
4272
4273 let first =
4274 run_sqlite_migrations(&first_set, &db_path_str).expect("first migrate succeeds");
4275 assert_eq!(first.applied_count, 1);
4276
4277 let second_set = Migrations::new(
4278 vec![
4279 Migration::with_hash(
4280 "20230331141203_first",
4281 "hash_one",
4282 1_680_271_923_000,
4283 vec!["CREATE TABLE created_at_dedupe_a (id INTEGER PRIMARY KEY)".to_string()],
4284 ),
4285 Migration::with_hash(
4286 "20230331141203_second",
4287 "hash_two",
4288 1_680_271_923_000,
4289 vec!["CREATE TABLE created_at_dedupe_b (id INTEGER PRIMARY KEY)".to_string()],
4290 ),
4291 ],
4292 drizzle_types::Dialect::SQLite,
4293 );
4294
4295 let second =
4296 run_sqlite_migrations(&second_set, &db_path_str).expect("second migrate succeeds");
4297 assert_eq!(
4298 second.applied_count, 1,
4299 "only the second (newly introduced by name) migration should apply"
4300 );
4301
4302 let conn = rusqlite::Connection::open(&db_path).expect("open sqlite");
4303 let rows: i64 = conn
4304 .query_row("SELECT COUNT(*) FROM __drizzle_migrations", [], |row| {
4305 row.get(0)
4306 })
4307 .expect("count migrations rows");
4308 assert_eq!(rows, 2, "both migration records should be stored");
4309
4310 let table_b_exists: i64 = conn
4311 .query_row(
4312 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='created_at_dedupe_b'",
4313 [],
4314 |row| row.get(0),
4315 )
4316 .expect("query sqlite_master");
4317 assert_eq!(
4318 table_b_exists, 1,
4319 "the new-name migration must execute even though created_at collides"
4320 );
4321 }
4322
4323 #[cfg(feature = "rusqlite")]
4324 #[test]
4325 fn run_migrations_creates_metadata_table_with_no_local_migrations() {
4326 let dir = tempfile::tempdir().expect("tempdir");
4327 let db_path = dir.path().join("empty.sqlite");
4328 let migrations_dir = dir.path().join("migrations");
4329 std::fs::create_dir_all(&migrations_dir).expect("create migrations dir");
4330
4331 let creds = crate::config::Credentials::Sqlite {
4332 path: db_path.to_string_lossy().to_string().into_boxed_str(),
4333 };
4334
4335 let result = run_migrations(
4336 &creds,
4337 crate::config::Dialect::Sqlite,
4338 &migrations_dir,
4339 "__drizzle_migrations",
4340 "drizzle",
4341 )
4342 .expect("run migrations");
4343 assert_eq!(result.applied_count, 0);
4344
4345 let conn = rusqlite::Connection::open(&db_path).expect("open sqlite");
4346 let exists: i64 = conn
4347 .query_row(
4348 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='__drizzle_migrations'",
4349 [],
4350 |row| row.get(0),
4351 )
4352 .expect("check metadata table");
4353 assert_eq!(exists, 1, "migrations metadata table should be created");
4354 }
4355
4356 #[cfg(any(
4357 feature = "rusqlite",
4358 feature = "libsql",
4359 feature = "turso",
4360 feature = "postgres-sync",
4361 feature = "tokio-postgres"
4362 ))]
4363 #[test]
4364 fn validate_init_metadata_matches_drizzle_orm_semantics() {
4365 use drizzle_migrations::{Migration, Migrations};
4366
4367 let empty_set = Migrations::empty(drizzle_types::Dialect::SQLite);
4368 validate_init_metadata(&[], &empty_set).expect("empty local migrations should be allowed");
4369
4370 let single = Migrations::new(
4371 vec![Migration::with_hash(
4372 "20230331141203_init",
4373 "hash_single",
4374 1_680_271_923_000,
4375 vec!["CREATE TABLE t(id INTEGER PRIMARY KEY)".to_string()],
4376 )],
4377 drizzle_types::Dialect::SQLite,
4378 );
4379 validate_init_metadata(&[], &single).expect("single local migration should be allowed");
4380
4381 let multiple = Migrations::new(
4382 vec![
4383 Migration::with_hash(
4384 "20230331141203_first",
4385 "hash_a",
4386 1_680_271_923_000,
4387 vec!["CREATE TABLE a(id INTEGER PRIMARY KEY)".to_string()],
4388 ),
4389 Migration::with_hash(
4390 "20230331150000_second",
4391 "hash_b",
4392 1_680_275_400_000,
4393 vec!["CREATE TABLE b(id INTEGER PRIMARY KEY)".to_string()],
4394 ),
4395 ],
4396 drizzle_types::Dialect::SQLite,
4397 );
4398
4399 let err =
4400 validate_init_metadata(&[], &multiple).expect_err("multiple local migrations rejected");
4401 assert_eq!(
4402 err.to_string(),
4403 "--init can't be used with existing migrations"
4404 );
4405
4406 let err = validate_init_metadata(&["20230331141203_init".to_string()], &single)
4407 .expect_err("existing db metadata should be rejected");
4408 assert_eq!(
4409 err.to_string(),
4410 "--init can't be used when database already has migrations set"
4411 );
4412 }
4413
4414 #[test]
4415 fn verify_applied_migrations_detects_hash_mismatch() {
4416 use drizzle_migrations::{Migration, Migrations};
4417
4418 let set = Migrations::new(
4419 vec![Migration::with_hash(
4420 "20230331141203_verify",
4421 "local_hash",
4422 1_680_271_923_000,
4423 vec!["CREATE TABLE t(id INTEGER PRIMARY KEY)".to_string()],
4424 )],
4425 drizzle_types::Dialect::SQLite,
4426 );
4427
4428 let applied = vec![AppliedMigrationRecord {
4429 hash: "db_hash".to_string(),
4430 name: "20230331141203_verify".to_string(),
4431 }];
4432
4433 let err = verify_applied_migrations_consistency(&set, &applied)
4434 .expect_err("hash mismatch should fail verification");
4435 assert_eq!(
4436 err.to_string(),
4437 "Migration failed: Migration hash mismatch for 20230331141203_verify: database=db_hash, local=local_hash"
4438 );
4439 }
4440
4441 #[test]
4442 fn build_migration_plan_counts_pending_statements() {
4443 use drizzle_migrations::{Migration, Migrations};
4444
4445 let set = Migrations::new(
4446 vec![
4447 Migration::with_hash(
4448 "20230331141203_first",
4449 "hash_a",
4450 1_680_271_923_000,
4451 vec![
4452 "CREATE TABLE a(id INTEGER PRIMARY KEY)".to_string(),
4453 "CREATE INDEX a_id_idx ON a(id)".to_string(),
4454 ],
4455 ),
4456 Migration::with_hash(
4457 "20230331150000_second",
4458 "hash_b",
4459 1_680_275_400_000,
4460 vec!["CREATE TABLE b(id INTEGER PRIMARY KEY)".to_string()],
4461 ),
4462 ],
4463 drizzle_types::Dialect::SQLite,
4464 );
4465
4466 let applied = vec![AppliedMigrationRecord {
4467 hash: "hash_a".to_string(),
4468 name: "20230331141203_first".to_string(),
4469 }];
4470
4471 let plan = build_migration_plan(&set, &applied).expect("build migration plan");
4472 assert_eq!(plan.applied_count, 1);
4473 assert_eq!(plan.pending_count, 1);
4474 assert_eq!(plan.pending_statements, 1);
4475 assert_eq!(plan.pending_migrations, vec!["20230331150000_second"]);
4476 }
4477
4478 #[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
4479 #[test]
4480 fn postgres_concurrent_index_detection_is_case_insensitive() {
4481 assert!(is_postgres_concurrent_index_statement(
4482 "CREATE INDEX CONCURRENTLY users_email_idx ON users (email);"
4483 ));
4484 assert!(is_postgres_concurrent_index_statement(
4485 "CREATE UNIQUE INDEX CONCURRENTLY users_email_idx ON users (email);"
4486 ));
4487 assert!(is_postgres_concurrent_index_statement(
4488 "drop index concurrently users_email_idx;"
4489 ));
4490 assert!(!is_postgres_concurrent_index_statement(
4491 "CREATE INDEX users_email_idx ON users (email);"
4492 ));
4493 }
4494
4495 #[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
4496 #[test]
4497 fn postgres_concurrent_index_detection_over_statement_list() {
4498 let with_concurrent = vec![
4499 "CREATE TABLE users(id integer);".to_string(),
4500 "CREATE INDEX CONCURRENTLY users_email_idx ON users (id);".to_string(),
4501 ];
4502 let without_concurrent = vec![
4503 "CREATE TABLE users(id integer);".to_string(),
4504 "CREATE INDEX users_email_idx ON users (id);".to_string(),
4505 ];
4506
4507 assert!(has_postgres_concurrent_index(&with_concurrent));
4508 assert!(!has_postgres_concurrent_index(&without_concurrent));
4509 }
4510
4511 #[test]
4512 fn generate_push_sql_includes_concurrent_postgres_index() {
4513 use crate::snapshot::parse_result_to_snapshot;
4514 use drizzle_migrations::parser::SchemaParser;
4515 use drizzle_types::Dialect;
4516
4517 let previous = r#"
4518#[PostgresTable]
4519pub struct Users {
4520 #[column(primary)]
4521 pub id: i32,
4522 pub email: String,
4523}
4524"#;
4525
4526 let current = r#"
4527#[PostgresTable]
4528pub struct Users {
4529 #[column(primary)]
4530 pub id: i32,
4531 pub email: String,
4532}
4533
4534#[PostgresIndex(concurrent)]
4535pub struct UsersEmailIdx(Users::email);
4536"#;
4537
4538 let prev =
4539 parse_result_to_snapshot(&SchemaParser::parse(previous), Dialect::PostgreSQL, None);
4540 let curr =
4541 parse_result_to_snapshot(&SchemaParser::parse(current), Dialect::PostgreSQL, None);
4542
4543 let (sql, warnings) = generate_push_sql(&prev, &curr, false).expect("push sql generation");
4544 assert!(warnings.is_empty());
4545 assert_eq!(sql.len(), 1);
4546 assert_eq!(
4547 sql[0],
4548 "CREATE INDEX CONCURRENTLY \"users_email_idx\" ON \"users\"(\"email\");"
4549 );
4550 }
4551
4552 #[test]
4553 fn filter_patterns_support_negation_globs() {
4554 let raw = vec![
4555 "users_*".to_string(),
4556 "!users_4".to_string(),
4557 "!ad*".to_string(),
4558 ];
4559 let patterns = compile_patterns(Some(&raw)).expect("compile patterns");
4560
4561 assert!(matches_patterns("users_1", patterns.as_deref()));
4562 assert!(!matches_patterns("users_4", patterns.as_deref()));
4563 assert!(!matches_patterns("admin", patterns.as_deref()));
4564 assert!(!matches_patterns("audit", patterns.as_deref()));
4565 }
4566
4567 #[test]
4568 fn postgres_table_filter_matches_table_name_only() {
4569 use crate::snapshot::parse_result_to_snapshot;
4570 use drizzle_migrations::parser::SchemaParser;
4571 use drizzle_migrations::schema::Snapshot;
4572 use drizzle_types::Dialect as BaseDialect;
4573
4574 let code = r#"
4575#[PostgresTable(schema = "admin")]
4576pub struct AuditLog {
4577 #[column(primary)]
4578 pub id: i32,
4579}
4580
4581#[PostgresTable(schema = "public")]
4582pub struct Users {
4583 #[column(primary)]
4584 pub id: i32,
4585}
4586"#;
4587
4588 let parsed = SchemaParser::parse(code);
4589 let mut snapshot = parse_result_to_snapshot(&parsed, BaseDialect::PostgreSQL, None);
4590 let filters = SnapshotFilters {
4591 tables: Some(vec!["admin.*".to_string()]),
4592 schemas: None,
4593 extensions: None,
4594 };
4595
4596 apply_snapshot_filters(&mut snapshot, crate::config::Dialect::Postgresql, &filters)
4597 .expect("apply filters");
4598
4599 let remaining_tables = match snapshot {
4600 Snapshot::Postgres(s) => s
4601 .ddl
4602 .iter()
4603 .filter_map(|e| {
4604 if let drizzle_types::postgres::ddl::PostgresEntity::Table(t) = e {
4605 Some((t.schema.to_string(), t.name.to_string()))
4606 } else {
4607 None
4608 }
4609 })
4610 .collect::<Vec<_>>(),
4611 _ => panic!("expected postgres snapshot"),
4612 };
4613
4614 assert!(remaining_tables.is_empty());
4615 }
4616
4617 #[test]
4618 fn postgres_schema_and_table_filters_intersect() {
4619 use crate::snapshot::parse_result_to_snapshot;
4620 use drizzle_migrations::parser::SchemaParser;
4621 use drizzle_migrations::schema::Snapshot;
4622 use drizzle_types::Dialect as BaseDialect;
4623
4624 let code = r#"
4625#[PostgresTable(schema = "dev")]
4626pub struct UsersDev {
4627 #[column(primary)]
4628 pub id: i32,
4629}
4630
4631#[PostgresTable(schema = "public")]
4632pub struct UsersPublic {
4633 #[column(primary)]
4634 pub id: i32,
4635}
4636"#;
4637
4638 let parsed = SchemaParser::parse(code);
4639 let mut snapshot = parse_result_to_snapshot(&parsed, BaseDialect::PostgreSQL, None);
4640 let filters = SnapshotFilters {
4641 tables: Some(vec!["users_*".to_string()]),
4642 schemas: Some(vec!["!dev".to_string()]),
4643 extensions: None,
4644 };
4645
4646 apply_snapshot_filters(&mut snapshot, crate::config::Dialect::Postgresql, &filters)
4647 .expect("apply filters");
4648
4649 let remaining_tables = match snapshot {
4650 Snapshot::Postgres(s) => s
4651 .ddl
4652 .iter()
4653 .filter_map(|e| {
4654 if let drizzle_types::postgres::ddl::PostgresEntity::Table(t) = e {
4655 Some((t.schema.to_string(), t.name.to_string()))
4656 } else {
4657 None
4658 }
4659 })
4660 .collect::<Vec<_>>(),
4661 _ => panic!("expected postgres snapshot"),
4662 };
4663
4664 assert_eq!(
4665 remaining_tables,
4666 vec![("public".to_string(), "users_public".to_string())]
4667 );
4668 }
4669
4670 #[test]
4671 fn postgres_extensions_filter_excludes_postgis_internal_objects() {
4672 use crate::snapshot::parse_result_to_snapshot;
4673 use drizzle_migrations::parser::SchemaParser;
4674 use drizzle_migrations::schema::Snapshot;
4675 use drizzle_types::Dialect as BaseDialect;
4676
4677 let code = r#"
4678#[PostgresTable(schema = "topology")]
4679pub struct TopologyLayer {
4680 #[column(primary)]
4681 pub id: i32,
4682}
4683
4684#[PostgresTable]
4685pub struct SpatialRefSys {
4686 #[column(primary)]
4687 pub id: i32,
4688}
4689
4690#[PostgresTable]
4691pub struct Users {
4692 #[column(primary)]
4693 pub id: i32,
4694}
4695"#;
4696
4697 let parsed = SchemaParser::parse(code);
4698 let mut snapshot = parse_result_to_snapshot(&parsed, BaseDialect::PostgreSQL, None);
4699 let filters = SnapshotFilters {
4700 tables: None,
4701 schemas: None,
4702 extensions: Some(vec![Extension::Postgis]),
4703 };
4704
4705 apply_snapshot_filters(&mut snapshot, crate::config::Dialect::Postgresql, &filters)
4706 .expect("apply filters");
4707
4708 let remaining_tables = match snapshot {
4709 Snapshot::Postgres(s) => s
4710 .ddl
4711 .iter()
4712 .filter_map(|e| {
4713 if let drizzle_types::postgres::ddl::PostgresEntity::Table(t) = e {
4714 Some((t.schema.to_string(), t.name.to_string()))
4715 } else {
4716 None
4717 }
4718 })
4719 .collect::<Vec<_>>(),
4720 _ => panic!("expected postgres snapshot"),
4721 };
4722
4723 assert_eq!(
4724 remaining_tables,
4725 vec![("public".to_string(), "users".to_string())]
4726 );
4727 }
4728
4729 #[test]
4730 fn format_migration_sql_respects_breakpoints_flag() {
4731 let sql = vec![
4732 "CREATE TABLE users(id integer);".to_string(),
4733 "CREATE INDEX users_id_idx ON users(id);".to_string(),
4734 ];
4735
4736 let with_breakpoints = format_migration_sql(&sql, true);
4737 assert_eq!(
4738 with_breakpoints,
4739 "CREATE TABLE users(id integer);\n--> statement-breakpoint\nCREATE INDEX users_id_idx ON users(id);"
4740 );
4741
4742 let without_breakpoints = format_migration_sql(&sql, false);
4743 assert_eq!(
4744 without_breakpoints,
4745 "CREATE TABLE users(id integer);\n\nCREATE INDEX users_id_idx ON users(id);"
4746 );
4747
4748 let empty = format_migration_sql(&[], false);
4749 assert_eq!(empty, "-- No tables to create (empty database)\n");
4750 }
4751
4752 #[test]
4753 fn regenerate_sqlite_schema_applies_introspect_casing() {
4754 use crate::config::{Casing, IntrospectCasing};
4755 use crate::snapshot::parse_result_to_snapshot;
4756 use drizzle_migrations::parser::SchemaParser;
4757 use drizzle_types::Dialect as BaseDialect;
4758
4759 let code = r#"
4760#[SQLiteTable]
4761pub struct AuditLogs {
4762 #[column(primary)]
4763 pub id: i64,
4764 pub user_name: String,
4765}
4766"#;
4767
4768 let parsed = SchemaParser::parse(code);
4769 let snapshot =
4770 parse_result_to_snapshot(&parsed, BaseDialect::SQLite, Some(Casing::SnakeCase));
4771
4772 let mut camel = IntrospectResult {
4773 schema_code: String::new(),
4774 table_count: 0,
4775 index_count: 0,
4776 view_count: 0,
4777 warnings: Vec::new(),
4778 snapshot: snapshot.clone(),
4779 snapshot_path: std::path::PathBuf::new(),
4780 };
4781 regenerate_schema_from_snapshot(
4782 &mut camel,
4783 crate::config::Dialect::Sqlite,
4784 Some(IntrospectCasing::Camel),
4785 );
4786
4787 assert_eq!(
4788 camel.schema_code,
4789 "\
4790//! Auto-generated SQLite schema from introspection
4791//!
4792//! Schema introspected from filtered database objects
4793
4794use drizzle::sqlite::prelude::*;
4795
4796#[SQLiteTable(name = \"audit_logs\")]
4797pub struct AuditLogs {
4798 #[column(primary)]
4799 pub id: i64,
4800 pub userName: String,
4801}
4802
4803#[derive(SQLiteSchema)]
4804pub struct Schema {
4805 pub auditLogs: AuditLogs,
4806}
4807"
4808 );
4809
4810 let mut preserve = IntrospectResult {
4811 schema_code: String::new(),
4812 table_count: 0,
4813 index_count: 0,
4814 view_count: 0,
4815 warnings: Vec::new(),
4816 snapshot,
4817 snapshot_path: std::path::PathBuf::new(),
4818 };
4819 regenerate_schema_from_snapshot(
4820 &mut preserve,
4821 crate::config::Dialect::Sqlite,
4822 Some(IntrospectCasing::Preserve),
4823 );
4824
4825 assert_eq!(
4826 preserve.schema_code,
4827 "\
4828//! Auto-generated SQLite schema from introspection
4829//!
4830//! Schema introspected from filtered database objects
4831
4832use drizzle::sqlite::prelude::*;
4833
4834#[SQLiteTable(name = \"audit_logs\")]
4835pub struct AuditLogs {
4836 #[column(primary)]
4837 pub id: i64,
4838 pub user_name: String,
4839}
4840
4841#[derive(SQLiteSchema)]
4842pub struct Schema {
4843 pub audit_logs: AuditLogs,
4844}
4845"
4846 );
4847 }
4848
4849 #[test]
4850 fn regenerate_postgres_schema_applies_introspect_casing() {
4851 use crate::config::{Casing, IntrospectCasing};
4852 use crate::snapshot::parse_result_to_snapshot;
4853 use drizzle_migrations::parser::SchemaParser;
4854 use drizzle_types::Dialect as BaseDialect;
4855
4856 let code = r#"
4857#[PostgresTable]
4858pub struct AuditLogs {
4859 #[column(primary)]
4860 pub id: i32,
4861 pub user_name: String,
4862}
4863"#;
4864
4865 let parsed = SchemaParser::parse(code);
4866 let snapshot =
4867 parse_result_to_snapshot(&parsed, BaseDialect::PostgreSQL, Some(Casing::SnakeCase));
4868
4869 let mut camel = IntrospectResult {
4870 schema_code: String::new(),
4871 table_count: 0,
4872 index_count: 0,
4873 view_count: 0,
4874 warnings: Vec::new(),
4875 snapshot: snapshot.clone(),
4876 snapshot_path: std::path::PathBuf::new(),
4877 };
4878 regenerate_schema_from_snapshot(
4879 &mut camel,
4880 crate::config::Dialect::Postgresql,
4881 Some(IntrospectCasing::Camel),
4882 );
4883
4884 assert_eq!(
4885 camel.schema_code,
4886 "\
4887//! Auto-generated PostgreSQL schema from introspection
4888//!
4889//! Schema introspected from filtered database objects
4890
4891use drizzle::postgres::prelude::*;
4892
4893#[PostgresTable]
4894pub struct AuditLogs {
4895 #[column(primary)]
4896 pub id: i32,
4897 pub userName: String,
4898}
4899
4900#[derive(PostgresSchema)]
4901pub struct Schema {
4902 pub auditLogs: AuditLogs,
4903}
4904"
4905 );
4906
4907 let mut preserve = IntrospectResult {
4908 schema_code: String::new(),
4909 table_count: 0,
4910 index_count: 0,
4911 view_count: 0,
4912 warnings: Vec::new(),
4913 snapshot,
4914 snapshot_path: std::path::PathBuf::new(),
4915 };
4916 regenerate_schema_from_snapshot(
4917 &mut preserve,
4918 crate::config::Dialect::Postgresql,
4919 Some(IntrospectCasing::Preserve),
4920 );
4921
4922 assert_eq!(
4923 preserve.schema_code,
4924 "\
4925//! Auto-generated PostgreSQL schema from introspection
4926//!
4927//! Schema introspected from filtered database objects
4928
4929use drizzle::postgres::prelude::*;
4930
4931#[PostgresTable]
4932pub struct AuditLogs {
4933 #[column(primary)]
4934 pub id: i32,
4935 pub user_name: String,
4936}
4937
4938#[derive(PostgresSchema)]
4939pub struct Schema {
4940 pub audit_logs: AuditLogs,
4941}
4942"
4943 );
4944 }
4945
4946 #[cfg(feature = "postgres-sync")]
4947 #[test]
4948 fn postgres_sync_migrate_applies_concurrent_index_without_transaction() {
4949 use drizzle_migrations::{Migration, Migrations};
4950 use drizzle_types::Dialect;
4951
4952 let creds = test_postgres_creds();
4953 let mut setup_client = match connect_postgres_sync(&creds) {
4954 Ok(c) => c,
4955 Err(e) => {
4956 eprintln!(
4957 "Skipping postgres_sync_migrate_applies_concurrent_index_without_transaction: {}",
4958 e
4959 );
4960 return;
4961 }
4962 };
4963
4964 let table = unique_pg_name("cli_sync_users");
4965 let index = format!("{}_email_idx", table);
4966 let migration_schema = unique_pg_name("cli_sync_mig");
4967
4968 setup_client
4969 .batch_execute(&format!(
4970 "DROP TABLE IF EXISTS \"{table}\" CASCADE; \
4971 CREATE TABLE \"{table}\" (id integer, email text NOT NULL); \
4972 INSERT INTO \"{table}\" (id, email) VALUES (1, 'a@example.com');"
4973 ))
4974 .expect("setup table for concurrent index test");
4975 drop(setup_client);
4976
4977 let migration_tag = format!("20260212000000_{table}");
4978 let migration_sql =
4979 format!("CREATE INDEX CONCURRENTLY \"{index}\" ON \"{table}\" (\"email\");");
4980 let set = Migrations::with_tracking(
4981 vec![Migration::new(&migration_tag, &migration_sql)],
4982 Dialect::PostgreSQL,
4983 drizzle_migrations::Tracking::POSTGRES.schema(migration_schema.clone()),
4984 );
4985
4986 let result = run_postgres_sync_migrations(&set, &creds)
4987 .expect("sync migration with concurrent index should succeed");
4988 assert_eq!(result.applied_count, 1);
4989
4990 let mut verify_client = connect_postgres_sync(&creds).expect("reconnect for verification");
4991 let exists: i64 = verify_client
4992 .query_one(
4993 "SELECT COUNT(*)::bigint FROM pg_indexes \
4994 WHERE schemaname = 'public' AND tablename = $1 AND indexname = $2",
4995 &[&table, &index],
4996 )
4997 .expect("query pg_indexes")
4998 .get(0);
4999 assert_eq!(exists, 1, "concurrent index was not created");
5000
5001 let _ = verify_client.batch_execute(&format!(
5002 "DROP TABLE IF EXISTS \"{table}\" CASCADE; \
5003 DROP SCHEMA IF EXISTS \"{migration_schema}\" CASCADE;"
5004 ));
5005 }
5006
5007 #[cfg(feature = "postgres-sync")]
5008 #[test]
5009 fn postgres_sync_migrate_upgrades_legacy_tracking_table_and_applies_pending() {
5010 use drizzle_migrations::{Migration, Migrations};
5011 use drizzle_types::Dialect;
5012
5013 let creds = test_postgres_creds();
5014 let mut client = connect_postgres_sync(&creds)
5015 .expect("connect postgres for legacy tracking upgrade test");
5016
5017 let applied_table = unique_pg_name("cli_sync_applied");
5018 let pending_table = unique_pg_name("cli_sync_pending");
5019 let migration_schema = unique_pg_name("cli_sync_tracking");
5020
5021 client
5022 .batch_execute(&format!(
5023 "DROP TABLE IF EXISTS \"{applied_table}\" CASCADE; \
5024 DROP TABLE IF EXISTS \"{pending_table}\" CASCADE; \
5025 DROP SCHEMA IF EXISTS \"{migration_schema}\" CASCADE; \
5026 CREATE SCHEMA \"{migration_schema}\"; \
5027 CREATE TABLE \"{migration_schema}\".\"__drizzle_migrations\" (id SERIAL PRIMARY KEY, hash TEXT NOT NULL, created_at BIGINT); \
5028 CREATE TABLE \"{applied_table}\" (id integer primary key);"
5029 ))
5030 .expect("setup legacy postgres tracking metadata");
5031
5032 let first = Migration::new(
5033 &format!("20230331141203_{applied_table}"),
5034 &format!("CREATE TABLE \"{applied_table}\" (id integer primary key);"),
5035 );
5036 let second = Migration::new(
5037 &format!("20230331141204_{pending_table}"),
5038 &format!("CREATE TABLE \"{pending_table}\" (id integer primary key);"),
5039 );
5040 let set = Migrations::with_tracking(
5041 vec![first.clone(), second.clone()],
5042 Dialect::PostgreSQL,
5043 drizzle_migrations::Tracking::POSTGRES.schema(migration_schema.clone()),
5044 );
5045
5046 client
5047 .execute(
5048 &format!(
5049 "INSERT INTO \"{migration_schema}\".\"__drizzle_migrations\" (hash, created_at) VALUES ($1, $2)"
5050 ),
5051 &[&first.hash(), &first.created_at()],
5052 )
5053 .expect("insert legacy applied migration row");
5054 drop(client);
5055
5056 let result = run_postgres_sync_migrations(&set, &creds)
5057 .expect("sync migration upgrade should succeed");
5058 assert_eq!(result.applied_count, 1);
5059 assert_eq!(result.applied_migrations, vec![second.hash().to_string()]);
5060
5061 let mut verify_client = connect_postgres_sync(&creds).expect("reconnect for verification");
5062 let columns: Vec<String> = verify_client
5063 .query(
5064 "SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = '__drizzle_migrations' ORDER BY ordinal_position",
5065 &[&migration_schema],
5066 )
5067 .expect("query upgraded tracking columns")
5068 .into_iter()
5069 .map(|row| row.get(0))
5070 .collect();
5071 assert_eq!(
5072 columns,
5073 vec!["id", "hash", "created_at", "name", "applied_at"]
5074 );
5075
5076 let rows: Vec<(String, i64, String, Option<String>)> = verify_client
5077 .query(
5078 &format!(
5079 "SELECT hash, created_at, name, applied_at::text FROM \"{migration_schema}\".\"__drizzle_migrations\" ORDER BY id ASC"
5080 ),
5081 &[],
5082 )
5083 .expect("query upgraded metadata rows")
5084 .into_iter()
5085 .map(|row| (row.get(0), row.get(1), row.get(2), row.get(3)))
5086 .collect();
5087 assert_eq!(rows.len(), 2);
5088 assert_eq!(rows[0].0, first.hash());
5089 assert_eq!(rows[0].1, first.created_at());
5090 assert_eq!(rows[0].2, first.name());
5091 assert_eq!(rows[0].3, None);
5092 assert_eq!(rows[1].0, second.hash());
5093 assert_eq!(rows[1].1, second.created_at());
5094 assert_eq!(rows[1].2, second.name());
5095 assert!(rows[1].3.is_some());
5096
5097 let pending_exists: i64 = verify_client
5098 .query_one(
5099 "SELECT COUNT(*)::bigint FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1",
5100 &[&pending_table],
5101 )
5102 .expect("query pending table")
5103 .get(0);
5104 assert_eq!(pending_exists, 1);
5105
5106 let _ = verify_client.batch_execute(&format!(
5107 "DROP TABLE IF EXISTS \"{applied_table}\" CASCADE; \
5108 DROP TABLE IF EXISTS \"{pending_table}\" CASCADE; \
5109 DROP SCHEMA IF EXISTS \"{migration_schema}\" CASCADE;"
5110 ));
5111 }
5112
5113 #[cfg(feature = "postgres-sync")]
5114 #[test]
5115 fn postgres_sync_migrate_upgrade_rejects_unmatched_legacy_rows() {
5116 use drizzle_migrations::{Migration, Migrations};
5117 use drizzle_types::Dialect;
5118
5119 let creds = test_postgres_creds();
5120 let mut client =
5121 connect_postgres_sync(&creds).expect("connect postgres for unmatched legacy row test");
5122
5123 let migration_schema = unique_pg_name("cli_sync_tracking_unmatched");
5124 client
5125 .batch_execute(&format!(
5126 "DROP SCHEMA IF EXISTS \"{migration_schema}\" CASCADE; \
5127 CREATE SCHEMA \"{migration_schema}\"; \
5128 CREATE TABLE \"{migration_schema}\".\"__drizzle_migrations\" (id SERIAL PRIMARY KEY, hash TEXT NOT NULL, created_at BIGINT);"
5129 ))
5130 .expect("setup unmatched legacy metadata");
5131
5132 let migration = Migration::new(
5133 "20230331141203_cli_sync_first",
5134 "CREATE TABLE \"cli_sync_unmatched_target\" (id integer primary key);",
5135 );
5136 let set = Migrations::with_tracking(
5137 vec![migration],
5138 Dialect::PostgreSQL,
5139 drizzle_migrations::Tracking::POSTGRES.schema(migration_schema.clone()),
5140 );
5141
5142 client
5143 .execute(
5144 &format!(
5145 "INSERT INTO \"{migration_schema}\".\"__drizzle_migrations\" (hash, created_at) VALUES ($1, $2)"
5146 ),
5147 &[&"unknown_hash", &1_680_271_924_000_i64],
5148 )
5149 .expect("insert unmatched legacy row");
5150 drop(client);
5151
5152 let err = run_postgres_sync_migrations(&set, &creds)
5153 .expect_err("unmatched legacy metadata should fail");
5154 assert!(err.to_string().contains("do not match local migrations"));
5155
5156 let mut verify_client = connect_postgres_sync(&creds).expect("reconnect for verification");
5157 let columns: Vec<String> = verify_client
5158 .query(
5159 "SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = '__drizzle_migrations' ORDER BY ordinal_position",
5160 &[&migration_schema],
5161 )
5162 .expect("query legacy tracking columns")
5163 .into_iter()
5164 .map(|row| row.get(0))
5165 .collect();
5166 assert_eq!(columns, vec!["id", "hash", "created_at"]);
5167
5168 let _ = verify_client.batch_execute(&format!(
5169 "DROP TABLE IF EXISTS \"cli_sync_unmatched_target\" CASCADE; \
5170 DROP SCHEMA IF EXISTS \"{migration_schema}\" CASCADE;"
5171 ));
5172 }
5173
5174 #[cfg(feature = "tokio-postgres")]
5175 #[test]
5176 fn tokio_postgres_migrate_applies_concurrent_index_without_transaction() {
5177 use drizzle_migrations::{Migration, Migrations};
5178 use drizzle_types::Dialect;
5179
5180 let creds = test_postgres_creds();
5181 let rt = tokio::runtime::Builder::new_current_thread()
5182 .enable_all()
5183 .build()
5184 .expect("create tokio runtime");
5185
5186 rt.block_on(async {
5187 let client = match connect_postgres_async(&creds).await {
5188 Ok(client) => client,
5189 Err(e) => {
5190 eprintln!(
5191 "Skipping tokio_postgres_migrate_applies_concurrent_index_without_transaction: {}",
5192 e
5193 );
5194 return;
5195 }
5196 };
5197
5198 let table = unique_pg_name("cli_async_users");
5199 let index = format!("{}_email_idx", table);
5200 let migration_schema = unique_pg_name("cli_async_mig");
5201
5202 client
5203 .batch_execute(&format!(
5204 "DROP TABLE IF EXISTS \"{table}\" CASCADE; \
5205 CREATE TABLE \"{table}\" (id integer, email text NOT NULL); \
5206 INSERT INTO \"{table}\" (id, email) VALUES (1, 'a@example.com');"
5207 ))
5208 .await
5209 .expect("setup table for async concurrent index test");
5210
5211 let migration_tag = format!("20260212000000_{table}");
5212 let migration_sql = format!(
5213 "CREATE INDEX CONCURRENTLY \"{index}\" ON \"{table}\" (\"email\");"
5214 );
5215 let set = Migrations::with_tracking(
5216 vec![Migration::new(&migration_tag, &migration_sql)],
5217 Dialect::PostgreSQL,
5218 drizzle_migrations::Tracking::POSTGRES.schema(migration_schema.clone()),
5219 );
5220
5221 let result = run_postgres_async_inner(&set, &creds)
5222 .await
5223 .expect("async migration with concurrent index should succeed");
5224 assert_eq!(result.applied_count, 1);
5225
5226 let exists: i64 = client
5227 .query_one(
5228 "SELECT COUNT(*)::bigint FROM pg_indexes \
5229 WHERE schemaname = 'public' AND tablename = $1 AND indexname = $2",
5230 &[&table, &index],
5231 )
5232 .await
5233 .expect("query pg_indexes")
5234 .get(0);
5235 assert_eq!(exists, 1, "async concurrent index was not created");
5236
5237 let _ = client
5238 .batch_execute(&format!(
5239 "DROP TABLE IF EXISTS \"{table}\" CASCADE; \
5240 DROP SCHEMA IF EXISTS \"{migration_schema}\" CASCADE;"
5241 ))
5242 .await;
5243 });
5244 }
5245
5246 #[cfg(feature = "tokio-postgres")]
5247 #[test]
5248 fn tokio_postgres_migrate_upgrades_legacy_tracking_table_and_applies_pending() {
5249 use drizzle_migrations::{Migration, Migrations};
5250 use drizzle_types::Dialect;
5251
5252 let creds = test_postgres_creds();
5253 let rt = tokio::runtime::Builder::new_current_thread()
5254 .enable_all()
5255 .build()
5256 .expect("create tokio runtime");
5257
5258 rt.block_on(async {
5259 let client = connect_postgres_async(&creds)
5260 .await
5261 .expect("connect tokio-postgres for legacy tracking upgrade test");
5262
5263 let applied_table = unique_pg_name("cli_async_applied");
5264 let pending_table = unique_pg_name("cli_async_pending");
5265 let migration_schema = unique_pg_name("cli_async_tracking");
5266
5267 client
5268 .batch_execute(&format!(
5269 "DROP TABLE IF EXISTS \"{applied_table}\" CASCADE; \
5270 DROP TABLE IF EXISTS \"{pending_table}\" CASCADE; \
5271 DROP SCHEMA IF EXISTS \"{migration_schema}\" CASCADE; \
5272 CREATE SCHEMA \"{migration_schema}\"; \
5273 CREATE TABLE \"{migration_schema}\".\"__drizzle_migrations\" (id SERIAL PRIMARY KEY, hash TEXT NOT NULL, created_at BIGINT); \
5274 CREATE TABLE \"{applied_table}\" (id integer primary key);"
5275 ))
5276 .await
5277 .expect("setup legacy postgres tracking metadata");
5278
5279 let first = Migration::new(
5280 &format!("20230331141203_{applied_table}"),
5281 &format!("CREATE TABLE \"{applied_table}\" (id integer primary key);"),
5282 );
5283 let second = Migration::new(
5284 &format!("20230331141204_{pending_table}"),
5285 &format!("CREATE TABLE \"{pending_table}\" (id integer primary key);"),
5286 );
5287 let set = Migrations::with_tracking(
5288 vec![first.clone(), second.clone()],
5289 Dialect::PostgreSQL,
5290 drizzle_migrations::Tracking::POSTGRES.schema(migration_schema.clone()),
5291 );
5292
5293 client
5294 .execute(
5295 &format!(
5296 "INSERT INTO \"{migration_schema}\".\"__drizzle_migrations\" (hash, created_at) VALUES ($1, $2)"
5297 ),
5298 &[&first.hash(), &first.created_at()],
5299 )
5300 .await
5301 .expect("insert legacy applied migration row");
5302
5303 let result = run_postgres_async_inner(&set, &creds)
5304 .await
5305 .expect("async migration upgrade should succeed");
5306 assert_eq!(result.applied_count, 1);
5307 assert_eq!(result.applied_migrations, vec![second.hash().to_string()]);
5308
5309 let columns: Vec<String> = client
5310 .query(
5311 "SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = '__drizzle_migrations' ORDER BY ordinal_position",
5312 &[&migration_schema],
5313 )
5314 .await
5315 .expect("query upgraded tracking columns")
5316 .into_iter()
5317 .map(|row| row.get(0))
5318 .collect();
5319 assert_eq!(
5320 columns,
5321 vec!["id", "hash", "created_at", "name", "applied_at"]
5322 );
5323
5324 let rows: Vec<(String, i64, String, Option<String>)> = client
5325 .query(
5326 &format!(
5327 "SELECT hash, created_at, name, applied_at::text FROM \"{migration_schema}\".\"__drizzle_migrations\" ORDER BY id ASC"
5328 ),
5329 &[],
5330 )
5331 .await
5332 .expect("query upgraded metadata rows")
5333 .into_iter()
5334 .map(|row| (row.get(0), row.get(1), row.get(2), row.get(3)))
5335 .collect();
5336 assert_eq!(rows.len(), 2);
5337 assert_eq!(rows[0].0, first.hash());
5338 assert_eq!(rows[0].1, first.created_at());
5339 assert_eq!(rows[0].2, first.name());
5340 assert_eq!(rows[0].3, None);
5341 assert_eq!(rows[1].0, second.hash());
5342 assert_eq!(rows[1].1, second.created_at());
5343 assert_eq!(rows[1].2, second.name());
5344 assert!(rows[1].3.is_some());
5345
5346 let pending_exists: i64 = client
5347 .query_one(
5348 "SELECT COUNT(*)::bigint FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1",
5349 &[&pending_table],
5350 )
5351 .await
5352 .expect("query pending table")
5353 .get(0);
5354 assert_eq!(pending_exists, 1);
5355
5356 let _ = client
5357 .batch_execute(&format!(
5358 "DROP TABLE IF EXISTS \"{applied_table}\" CASCADE; \
5359 DROP TABLE IF EXISTS \"{pending_table}\" CASCADE; \
5360 DROP SCHEMA IF EXISTS \"{migration_schema}\" CASCADE;"
5361 ))
5362 .await;
5363 });
5364 }
5365}