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