1use std::path::Path;
8
9use crate::commands::overrides::ResolvedConnection;
10#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
11use crate::config::PostgresCreds;
12use crate::config::{Credentials, Dialect, Driver, Extension, IntrospectCasing};
13use crate::error::CliError;
14use crate::output;
15use drizzle_migrations::Migrations;
16use drizzle_migrations::schema::Snapshot;
17
18#[cfg(feature = "d1-http")]
19mod d1_http;
20mod filters;
21mod mysql;
22
23pub use filters::apply_snapshot_filters;
24#[cfg(test)]
25use filters::{compile_patterns, matches_patterns};
26
27#[derive(Debug, Default)]
29pub struct MigrationResult {
30 pub applied_count: usize,
32 pub applied_migrations: Vec<String>,
34 pub repaired_migrations: Vec<String>,
36}
37
38#[derive(Debug, Clone)]
40pub struct MigrationPlan {
41 pub applied_count: usize,
43 pub pending_count: usize,
45 pub pending_migrations: Vec<String>,
47 pub pending_statements: usize,
49 pub findings: Vec<String>,
54}
55
56#[cfg(any(
59 feature = "rusqlite",
60 feature = "libsql",
61 feature = "turso",
62 feature = "postgres-sync",
63 feature = "tokio-postgres",
64 feature = "mysql-sync",
65 feature = "mysql-async",
66))]
67pub(crate) enum TrackingState {
68 Missing,
70 Legacy(Vec<drizzle_migrations::AppliedMigrationMetadata>),
72 Current,
74}
75
76#[cfg(any(
79 feature = "rusqlite",
80 feature = "libsql",
81 feature = "turso",
82 feature = "postgres-sync",
83 feature = "tokio-postgres",
84 feature = "mysql-sync",
85 feature = "mysql-async",
86))]
87pub(crate) fn legacy_applied_records(
88 set: &Migrations,
89 rows: &[drizzle_migrations::AppliedMigrationMetadata],
90) -> Result<Vec<AppliedMigrationRecord>, CliError> {
91 let matched = drizzle_migrations::match_applied_migration_metadata(set.all(), rows)
92 .map_err(|e| CliError::MigrationError(e.to_string()))?;
93 Ok(matched
94 .into_iter()
95 .map(|row| AppliedMigrationRecord {
96 hash: row.hash,
97 name: row.name,
98 dirty: false,
99 })
100 .collect())
101}
102
103#[cfg(any(feature = "rusqlite", feature = "libsql"))]
106fn sqlite_database_exists(path: &str) -> bool {
107 path == ":memory:" || path.starts_with("file:") || std::path::Path::new(path).exists()
108}
109
110#[cfg(any(
111 feature = "rusqlite",
112 feature = "libsql",
113 feature = "turso",
114 feature = "postgres-sync",
115 feature = "tokio-postgres",
116 feature = "mysql-sync",
117 feature = "mysql-async",
118 feature = "d1-http",
119))]
120#[derive(Debug, Clone)]
121pub(crate) struct AppliedMigrationRecord {
122 pub(crate) hash: String,
123 pub(crate) name: String,
124 pub(crate) dirty: bool,
126}
127
128#[derive(Debug, Clone)]
130pub struct PushPlan {
131 pub sql_statements: Vec<String>,
133 pub warnings: Vec<String>,
135 pub destructive: bool,
140}
141
142#[derive(Debug, Clone, Default)]
144pub struct SnapshotFilters {
145 pub tables: Option<Vec<String>>,
147 pub schemas: Option<Vec<String>>,
149 pub extensions: Option<Vec<Extension>>,
151 pub roles: Option<crate::config::RolesFilter>,
154}
155
156fn connection_driver_mismatch(connection: &ResolvedConnection) -> CliError {
157 CliError::Other(format!(
158 "driver '{}' cannot use the resolved {} credentials",
159 connection.driver, connection.dialect
160 ))
161}
162
163impl SnapshotFilters {
164 const fn is_empty(&self) -> bool {
165 self.tables.is_none() && self.schemas.is_none() && self.extensions.is_none()
166 }
167}
168
169pub fn plan_push(
183 connection: &ResolvedConnection,
184 desired: &Snapshot,
185 breakpoints: bool,
186 filters: &SnapshotFilters,
187 migrations_table: &str,
188) -> Result<PushPlan, CliError> {
189 plan_push_with_renames(
190 connection,
191 desired,
192 breakpoints,
193 filters,
194 migrations_table,
195 &mut |_, _, options| Ok(options),
196 )
197}
198
199pub fn plan_push_with_renames(
212 connection: &ResolvedConnection,
213 desired: &Snapshot,
214 breakpoints: bool,
215 filters: &SnapshotFilters,
216 migrations_table: &str,
217 resolve_renames: &mut dyn FnMut(
218 &Snapshot,
219 &Snapshot,
220 drizzle_migrations::DiffOptions,
221 ) -> Result<drizzle_migrations::DiffOptions, CliError>,
222) -> Result<PushPlan, CliError> {
223 let introspected = introspect_database(connection)?;
224 let mut current = introspected.snapshot;
225 exclude_tracking_table(&mut current, connection.dialect, migrations_table)?;
226 apply_snapshot_filters(&mut current, connection.dialect, filters)?;
227 if let (Snapshot::Postgres(live), Snapshot::Postgres(wanted)) = (&mut current, desired) {
230 live.align_push_insensitive_fields(wanted);
231 }
232 let mut options = drizzle_migrations::DiffOptions::new();
233 if let Some(defaults) = introspected.mysql_catalog_defaults {
234 options = options.mysql_catalog_defaults(defaults);
235 }
236 let options = resolve_renames(¤t, desired, options)?;
238 let drizzle_migrations::Plan {
239 statements: sql_statements,
240 mut warnings,
241 data_loss,
242 ..
243 } = generate_push_sql(¤t, desired, breakpoints, &options)?;
244 let lost = count_data_loss(connection, data_loss)?;
245 let destructive =
246 push_requires_confirmation(connection.dialect, &sql_statements, &warnings, &lost);
247 warnings.extend(lost.iter().filter_map(|(drop, rows)| match rows {
248 Some(0) => None,
249 Some(rows) if drop.column.is_some() => Some(format!(
250 "{drop} holds {rows} non-NULL value(s); push drops it"
251 )),
252 Some(rows) => Some(format!("{drop} holds {rows} row(s); push drops it")),
253 None => Some(format!(
254 "push drops {drop}, which could not be checked for data"
255 )),
256 }));
257
258 Ok(PushPlan {
259 sql_statements,
260 warnings,
261 destructive,
262 })
263}
264
265fn exclude_tracking_table(
266 snapshot: &mut Snapshot,
267 dialect: Dialect,
268 migrations_table: &str,
269) -> Result<(), CliError> {
270 if migrations_table.trim().is_empty() {
271 return Ok(());
272 }
273 apply_snapshot_filters(
274 snapshot,
275 dialect,
276 &SnapshotFilters {
277 tables: Some(vec![format!(
278 "!{}",
279 glob::Pattern::escape(migrations_table)
280 )]),
281 schemas: None,
282 extensions: None,
283 roles: None,
284 },
285 )
286}
287
288pub fn introspect_for_seed(
296 connection: &ResolvedConnection,
297 filters: &SnapshotFilters,
298 migrations_table: &str,
299) -> Result<Snapshot, CliError> {
300 let mut snapshot = introspect_database(connection)?.snapshot;
301 exclude_tracking_table(&mut snapshot, connection.dialect, migrations_table)?;
302 apply_snapshot_filters(&mut snapshot, connection.dialect, filters)?;
303 Ok(snapshot)
304}
305
306pub fn execute_seed(
314 connection: &ResolvedConnection,
315 statements: &[String],
316) -> Result<(), CliError> {
317 execute_statements(connection, statements)
318}
319
320pub fn apply_push(
329 connection: &ResolvedConnection,
330 plan: &PushPlan,
331 force: bool,
332) -> Result<(), CliError> {
333 apply_push_with_confirmation(connection, plan, force, confirm_destructive)
334}
335
336fn apply_push_with_confirmation(
338 connection: &ResolvedConnection,
339 plan: &PushPlan,
340 force: bool,
341 confirm: impl FnOnce() -> Result<bool, CliError>,
342) -> Result<(), CliError> {
343 if plan.sql_statements.is_empty() {
344 return Ok(());
345 }
346
347 if plan.destructive && !force && !confirm()? {
348 return Err(CliError::Aborted(
349 "Push aborted: the destructive changes were not confirmed, so nothing was applied"
350 .into(),
351 ));
352 }
353
354 execute_statements(connection, &plan.sql_statements)
355}
356
357#[allow(unused_variables)] pub fn plan_migrations(
367 connection: &ResolvedConnection,
368 migrations_dir: &Path,
369 migrations_table: &str,
370 migrations_schema: &str,
371) -> Result<MigrationPlan, CliError> {
372 let set = load_migration_set(
373 connection.dialect,
374 migrations_dir,
375 migrations_table,
376 migrations_schema,
377 )?;
378
379 match &connection.credentials {
380 #[cfg(feature = "rusqlite")]
381 Credentials::Sqlite { path } => inspect_sqlite_migrations(&set, path),
382
383 #[cfg(not(feature = "rusqlite"))]
384 Credentials::Sqlite { .. } => Err(CliError::MissingDriver {
385 dialect: "SQLite",
386 feature: "rusqlite",
387 }),
388
389 #[cfg(any(feature = "libsql", feature = "turso"))]
390 Credentials::Turso { url, auth_token } => {
391 if connection.driver == Driver::Libsql {
392 #[cfg(feature = "libsql")]
393 {
394 inspect_libsql_local_migrations(&set, url)
395 }
396 #[cfg(not(feature = "libsql"))]
397 {
398 Err(CliError::MissingDriver {
399 dialect: "LibSQL (local)",
400 feature: "libsql",
401 })
402 }
403 } else if connection.driver == Driver::Turso {
404 #[cfg(feature = "turso")]
405 {
406 inspect_turso_migrations(&set, url, auth_token.as_deref())
407 }
408 #[cfg(not(feature = "turso"))]
409 {
410 let _ = auth_token;
411 Err(CliError::MissingDriver {
412 dialect: "Turso (remote)",
413 feature: "turso",
414 })
415 }
416 } else {
417 Err(connection_driver_mismatch(connection))
418 }
419 }
420
421 #[cfg(all(not(feature = "turso"), not(feature = "libsql")))]
422 Credentials::Turso { .. } => Err(CliError::MissingDriver {
423 dialect: "Turso",
424 feature: "turso or libsql",
425 }),
426
427 Credentials::Postgres(creds) => {
428 let _ = creds;
429 match connection.driver {
430 Driver::PostgresSync => {
431 #[cfg(feature = "postgres-sync")]
432 {
433 inspect_postgres_sync_migrations(&set, creds)
434 }
435 #[cfg(not(feature = "postgres-sync"))]
436 {
437 Err(CliError::MissingDriver {
438 dialect: "PostgreSQL",
439 feature: "postgres-sync",
440 })
441 }
442 }
443 Driver::TokioPostgres => {
444 #[cfg(feature = "tokio-postgres")]
445 {
446 inspect_postgres_async_migrations(&set, creds)
447 }
448 #[cfg(not(feature = "tokio-postgres"))]
449 {
450 Err(CliError::MissingDriver {
451 dialect: "PostgreSQL",
452 feature: "tokio-postgres",
453 })
454 }
455 }
456 _ => Err(connection_driver_mismatch(connection)),
457 }
458 }
459
460 Credentials::MySQL(creds) => mysql::plan_migrations(connection.driver, creds, &set),
461
462 #[cfg(feature = "d1-http")]
463 Credentials::D1 {
464 account_id,
465 database_id,
466 token,
467 } => d1_http::inspect_migrations(&set, account_id, database_id, token),
468
469 #[cfg(not(feature = "d1-http"))]
470 Credentials::D1 { .. } => Err(CliError::MissingDriver {
471 dialect: "Cloudflare D1 (HTTP)",
472 feature: "d1-http",
473 }),
474
475 Credentials::AwsDataApi { .. } => Err(CliError::UnsupportedForDriver {
476 operation: "Migration planning against AWS Data API",
477 driver: "aws-data-api",
478 hint: "AWS RDS Data API schema ops are not yet wired into this CLI. For now, \
479 run the generated SQL with `aws rds-data execute-statement --sql=...` \
480 (or use tokio-postgres with a temporary direct connection).",
481 }),
482 }
483}
484
485#[allow(unused_variables)] pub fn run_migrations(
500 connection: &ResolvedConnection,
501 migrations_dir: &Path,
502 migrations_table: &str,
503 migrations_schema: &str,
504 repair: bool,
505) -> Result<MigrationResult, CliError> {
506 let set = load_migration_set(
507 connection.dialect,
508 migrations_dir,
509 migrations_table,
510 migrations_schema,
511 )?;
512
513 match &connection.credentials {
514 #[cfg(feature = "rusqlite")]
515 Credentials::Sqlite { path } => run_sqlite_migrations(&set, path, repair),
516
517 #[cfg(not(feature = "rusqlite"))]
518 Credentials::Sqlite { .. } => Err(CliError::MissingDriver {
519 dialect: "SQLite",
520 feature: "rusqlite",
521 }),
522
523 #[cfg(any(feature = "libsql", feature = "turso"))]
524 Credentials::Turso { url, auth_token } => {
525 if connection.driver == Driver::Libsql {
526 #[cfg(feature = "libsql")]
527 {
528 run_libsql_local_migrations(&set, url, repair)
529 }
530 #[cfg(not(feature = "libsql"))]
531 {
532 Err(CliError::MissingDriver {
533 dialect: "LibSQL (local)",
534 feature: "libsql",
535 })
536 }
537 } else if connection.driver == Driver::Turso {
538 #[cfg(feature = "turso")]
539 {
540 run_turso_migrations(&set, url, auth_token.as_deref(), repair)
541 }
542 #[cfg(not(feature = "turso"))]
543 {
544 let _ = auth_token;
545 Err(CliError::MissingDriver {
546 dialect: "Turso (remote)",
547 feature: "turso",
548 })
549 }
550 } else {
551 Err(connection_driver_mismatch(connection))
552 }
553 }
554
555 #[cfg(all(not(feature = "turso"), not(feature = "libsql")))]
556 Credentials::Turso { .. } => Err(CliError::MissingDriver {
557 dialect: "Turso",
558 feature: "turso or libsql",
559 }),
560
561 Credentials::Postgres(creds) => {
562 let _ = creds;
563 match connection.driver {
564 Driver::PostgresSync => {
565 #[cfg(feature = "postgres-sync")]
566 {
567 run_postgres_sync_migrations(&set, creds, repair)
568 }
569 #[cfg(not(feature = "postgres-sync"))]
570 {
571 Err(CliError::MissingDriver {
572 dialect: "PostgreSQL",
573 feature: "postgres-sync",
574 })
575 }
576 }
577 Driver::TokioPostgres => {
578 #[cfg(feature = "tokio-postgres")]
579 {
580 run_postgres_async_migrations(&set, creds, repair)
581 }
582 #[cfg(not(feature = "tokio-postgres"))]
583 {
584 Err(CliError::MissingDriver {
585 dialect: "PostgreSQL",
586 feature: "tokio-postgres",
587 })
588 }
589 }
590 _ => Err(connection_driver_mismatch(connection)),
591 }
592 }
593
594 Credentials::MySQL(creds) => mysql::run_migrations(connection.driver, creds, &set, repair),
595
596 #[cfg(feature = "d1-http")]
597 Credentials::D1 {
598 account_id,
599 database_id,
600 token,
601 } => d1_http::run_migrations(&set, account_id, database_id, token, repair),
602
603 #[cfg(not(feature = "d1-http"))]
604 Credentials::D1 { .. } => Err(CliError::MissingDriver {
605 dialect: "Cloudflare D1 (HTTP)",
606 feature: "d1-http",
607 }),
608
609 Credentials::AwsDataApi { .. } => Err(CliError::UnsupportedForDriver {
610 operation: "Running migrations against AWS Data API",
611 driver: "aws-data-api",
612 hint: "AWS RDS Data API migrations are not yet wired into this CLI. Run the \
613 generated SQL via `aws rds-data execute-statement` or connect directly \
614 with tokio-postgres.",
615 }),
616 }
617}
618
619fn load_migration_set(
620 dialect: Dialect,
621 migrations_dir: &Path,
622 migrations_table: &str,
623 migrations_schema: &str,
624) -> Result<Migrations, CliError> {
625 let tracking = migration_tracking(dialect, migrations_table, migrations_schema);
626
627 let migrations = drizzle_migrations::MigrationDir::new(migrations_dir)
629 .dialect(dialect.to_base())
630 .discover()
631 .map_err(|e| CliError::Other(format!("Failed to load migrations: {e}")))?;
632 Ok(Migrations::with_tracking(
633 migrations,
634 dialect.to_base(),
635 tracking,
636 ))
637}
638
639fn migration_tracking(
640 dialect: Dialect,
641 migrations_table: &str,
642 migrations_schema: &str,
643) -> drizzle_types::MigrationTracking {
644 let mut tracking = match dialect {
645 Dialect::Postgresql => drizzle_types::MigrationTracking::POSTGRES,
646 Dialect::Mysql => drizzle_types::MigrationTracking::MYSQL,
647 Dialect::Sqlite | Dialect::Turso => drizzle_types::MigrationTracking::SQLITE,
648 };
649
650 if !migrations_table.trim().is_empty() {
651 tracking = tracking.table(migrations_table.to_owned());
652 }
653
654 if dialect == Dialect::Postgresql && !migrations_schema.trim().is_empty() {
655 tracking = tracking.schema(migrations_schema.to_owned());
656 }
657
658 tracking
659}
660
661#[cfg(any(
662 feature = "rusqlite",
663 feature = "libsql",
664 feature = "turso",
665 feature = "postgres-sync",
666 feature = "tokio-postgres",
667 feature = "mysql-sync",
668 feature = "mysql-async",
669 feature = "d1-http",
670))]
671pub(crate) fn build_migration_plan(
672 set: &Migrations,
673 applied: &[AppliedMigrationRecord],
674) -> Result<MigrationPlan, CliError> {
675 let findings = collect_integrity_findings(set, applied)?;
676
677 let applied_names = applied
679 .iter()
680 .filter(|m| !m.dirty)
681 .map(|m| m.name.clone())
682 .collect::<Vec<_>>();
683 let pending = set.pending(&applied_names).collect::<Vec<_>>();
684
685 let pending_statements = pending
686 .iter()
687 .map(|m| {
688 m.statements()
689 .iter()
690 .filter(|stmt| !stmt.trim().is_empty())
691 .count()
692 })
693 .sum();
694
695 Ok(MigrationPlan {
696 applied_count: applied_names.len(),
697 pending_count: pending.len(),
698 pending_migrations: pending.iter().map(|m| m.tag().to_string()).collect(),
699 pending_statements,
700 findings,
701 })
702}
703
704#[cfg(any(
712 feature = "rusqlite",
713 feature = "libsql",
714 feature = "turso",
715 feature = "postgres-sync",
716 feature = "tokio-postgres",
717 feature = "mysql-sync",
718 feature = "mysql-async",
719 feature = "d1-http",
720))]
721fn collect_integrity_findings(
722 set: &Migrations,
723 applied: &[AppliedMigrationRecord],
724) -> Result<Vec<String>, CliError> {
725 use std::collections::{HashMap, HashSet};
726
727 let mut local_by_name = HashMap::<&str, &str>::new();
728 for migration in set.all() {
729 if local_by_name
730 .insert(migration.name(), migration.hash())
731 .is_some()
732 {
733 return Err(CliError::MigrationError(format!(
734 "Local migrations contain duplicate name: {}",
735 migration.name()
736 )));
737 }
738 }
739
740 let mut findings = Vec::new();
741 let mut seen_db_names = HashSet::<&str>::new();
742 for applied_row in applied {
743 if !seen_db_names.insert(applied_row.name.as_str()) {
744 findings.push(format!(
745 "duplicate tracking rows for `{}` in the migrations table",
746 applied_row.name
747 ));
748 continue;
749 }
750
751 if applied_row.dirty {
752 let recovery = if set.dialect() == drizzle_types::Dialect::MySQL {
753 "inspect the partially applied DDL and resolve it manually"
754 } else {
755 "run `drizzle migrate --repair` or resolve it manually"
756 };
757 findings.push(format!(
758 "`{}` was interrupted mid-apply (started but never finished) — {recovery}",
759 applied_row.name,
760 ));
761 }
762
763 let Some(local_hash) = local_by_name.get(applied_row.name.as_str()) else {
764 findings.push(format!(
765 "`{}` is recorded in the database but no local migration has that name",
766 applied_row.name
767 ));
768 continue;
769 };
770
771 if *local_hash != applied_row.hash {
772 findings.push(format!(
773 "`{}` has drifted: the local migration file no longer matches what \
774 was applied (database hash {}, local hash {})",
775 applied_row.name, applied_row.hash, local_hash
776 ));
777 }
778 }
779
780 Ok(findings)
781}
782
783#[cfg(any(feature = "rusqlite", feature = "libsql", feature = "turso"))]
784fn escape_sql_literal(value: &str) -> String {
785 value.replace('\'', "''")
786}
787
788#[cfg(any(
799 feature = "rusqlite",
800 feature = "libsql",
801 feature = "turso",
802 feature = "postgres-sync",
803 feature = "tokio-postgres",
804))]
805fn migrator_cli_error(error: drizzle_migrations::MigratorError) -> CliError {
806 CliError::MigrationError(error.to_string())
807}
808
809#[cfg(any(
812 feature = "rusqlite",
813 feature = "libsql",
814 feature = "turso",
815 feature = "postgres-sync",
816 feature = "tokio-postgres",
817))]
818fn dirty_migrations_to_repair<'a>(
819 set: &'a Migrations,
820 dirty: &[String],
821 repair: bool,
822) -> Result<Vec<&'a drizzle_migrations::Migration>, CliError> {
823 if dirty.is_empty() {
824 return Ok(Vec::new());
825 }
826 if !repair {
827 return Err(migrator_cli_error(
828 set.interrupted_migration_error(dirty)
829 .expect("dirty list is non-empty"),
830 ));
831 }
832 set.resolve_dirty_migrations(dirty)
833 .map_err(migrator_cli_error)
834}
835
836#[cfg(any(
837 feature = "rusqlite",
838 feature = "libsql",
839 feature = "turso",
840 feature = "d1-http",
841))]
842fn sqlite_execution_cli_error(
843 error: drizzle_migrations::SqliteMigrationExecutionError,
844) -> CliError {
845 CliError::MigrationError(error.to_string())
846}
847
848#[cfg(any(
849 feature = "rusqlite",
850 feature = "libsql",
851 feature = "turso",
852 feature = "d1-http",
853))]
854#[cfg(feature = "d1-http")]
855pub(super) fn reject_sqlite_foreign_key_suspension<'a>(
856 migrations: impl IntoIterator<Item = &'a drizzle_migrations::Migration>,
857 adapter: &str,
858) -> Result<(), CliError> {
859 let executions = migrations
860 .into_iter()
861 .map(drizzle_migrations::Migration::sqlite_execution)
862 .collect::<Result<Vec<_>, _>>()
863 .map_err(sqlite_execution_cli_error)?;
864 if executions
865 .iter()
866 .any(|execution| execution.suspends_foreign_keys())
867 {
868 return Err(CliError::MigrationError(format!(
869 "{adapter} cannot execute SQLite table rebuilds that suspend foreign keys"
870 )));
871 }
872 Ok(())
873}
874
875#[cfg(any(feature = "rusqlite", feature = "libsql", feature = "turso"))]
876fn ensure_sqlite_dirty_repairs_are_safe(
877 migrations: &[&drizzle_migrations::Migration],
878) -> Result<(), CliError> {
879 for migration in migrations {
880 let execution = migration
881 .sqlite_execution()
882 .map_err(sqlite_execution_cli_error)?;
883 if execution.suspends_foreign_keys() {
884 return Err(CliError::MigrationError(format!(
885 "Migration '{}' cannot be repaired safely: foreign-key-suspending table rebuilds require the original connection-owned transaction scope",
886 migration.tag()
887 )));
888 }
889 }
890 Ok(())
891}
892
893#[cfg(any(feature = "rusqlite", feature = "libsql", feature = "turso"))]
894fn finish_sqlite_foreign_key_scope<T>(
895 result: Result<T, CliError>,
896 restore: Result<(), CliError>,
897) -> Result<T, CliError> {
898 match (result, restore) {
899 (Err(primary), Err(restore)) => Err(CliError::MigrationError(format!(
900 "{primary}; additionally failed to restore SQLite foreign-key enforcement: {restore}"
901 ))),
902 (Err(primary), Ok(())) => Err(primary),
903 (Ok(_), Err(restore)) => Err(restore),
904 (Ok(value), Ok(())) => Ok(value),
905 }
906}
907
908#[cfg(feature = "rusqlite")]
909fn introspect_catalog_sqlite(
910 conn: &rusqlite::Connection,
911) -> Result<drizzle_migrations::repair::Catalog, CliError> {
912 let mut stmt = conn
913 .prepare(drizzle_migrations::repair::sqlite::OBJECTS_QUERY)
914 .map_err(|e| CliError::MigrationError(e.to_string()))?;
915 let rows = stmt
916 .query_map([], |row| {
917 Ok((
918 row.get::<_, String>(0)?,
919 row.get::<_, String>(1)?,
920 row.get::<_, Option<String>>(2)?,
921 ))
922 })
923 .map_err(|e| CliError::MigrationError(e.to_string()))?;
924
925 let rows = rows
926 .collect::<Result<Vec<_>, _>>()
927 .map_err(|e| CliError::MigrationError(e.to_string()))?;
928 Ok(drizzle_migrations::repair::sqlite::catalog(&rows))
929}
930
931#[cfg(feature = "rusqlite")]
932fn repair_dirty_migrations_sqlite(
933 conn: &rusqlite::Connection,
934 set: &Migrations,
935 repair: bool,
936) -> Result<Vec<String>, CliError> {
937 let dirty = {
938 let mut stmt = match conn.prepare(&set.dirty_names_sql()) {
939 Ok(stmt) => stmt,
940 Err(error) if is_sqlite_missing_table(&error) => return Ok(Vec::new()),
941 Err(error) => return Err(CliError::MigrationError(error.to_string())),
942 };
943 stmt.query_map([], |row| row.get::<_, String>(0))
944 .map_err(|e| CliError::MigrationError(e.to_string()))?
945 .collect::<Result<Vec<String>, _>>()
946 .map_err(|e| CliError::MigrationError(e.to_string()))?
947 };
948
949 let migrations = dirty_migrations_to_repair(set, &dirty, repair)?;
950 ensure_sqlite_dirty_repairs_are_safe(&migrations)?;
951 let table_ident = set.table_ident_sql();
952 let mut repaired = Vec::new();
953 for migration in migrations {
954 let catalog = introspect_catalog_sqlite(conn)?;
955 let plan =
956 drizzle_migrations::repair::plan(drizzle_types::Dialect::SQLite, migration, &catalog);
957 for statement in plan
958 .into_executable(&table_ident)
959 .map_err(migrator_cli_error)?
960 {
961 conn.execute(&statement, [])
962 .map_err(|e| CliError::MigrationError(format!("Repair statement failed: {e}")))?;
963 }
964 conn.execute(&set.record_migration_finished_sql(migration), [])
965 .map_err(|e| CliError::MigrationError(e.to_string()))?;
966 repaired.push(migration.tag().to_string());
967 }
968 Ok(repaired)
969}
970
971#[cfg(any(feature = "libsql", feature = "turso"))]
972async fn introspect_catalog_libsql(
973 conn: &libsql::Connection,
974) -> Result<drizzle_migrations::repair::Catalog, CliError> {
975 let mut rows = conn
976 .query(drizzle_migrations::repair::sqlite::OBJECTS_QUERY, ())
977 .await
978 .map_err(|e| CliError::MigrationError(e.to_string()))?;
979
980 let mut master_rows = Vec::new();
981 while let Some(row) = rows
982 .next()
983 .await
984 .map_err(|e| CliError::MigrationError(e.to_string()))?
985 {
986 master_rows.push((
987 row.get::<String>(0)
988 .map_err(|e| CliError::MigrationError(e.to_string()))?,
989 row.get::<String>(1)
990 .map_err(|e| CliError::MigrationError(e.to_string()))?,
991 row.get::<Option<String>>(2).ok().flatten(),
992 ));
993 }
994 Ok(drizzle_migrations::repair::sqlite::catalog(&master_rows))
995}
996
997#[cfg(any(feature = "libsql", feature = "turso"))]
998async fn repair_dirty_migrations_libsql(
999 conn: &libsql::Connection,
1000 set: &Migrations,
1001 repair: bool,
1002) -> Result<Vec<String>, CliError> {
1003 let mut rows = conn
1004 .query(&set.dirty_names_sql(), ())
1005 .await
1006 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1007 let mut dirty = Vec::new();
1008 while let Some(row) = rows
1009 .next()
1010 .await
1011 .map_err(|e| CliError::MigrationError(e.to_string()))?
1012 {
1013 dirty.push(
1014 row.get::<String>(0)
1015 .map_err(|e| CliError::MigrationError(e.to_string()))?,
1016 );
1017 }
1018
1019 let migrations = dirty_migrations_to_repair(set, &dirty, repair)?;
1020 ensure_sqlite_dirty_repairs_are_safe(&migrations)?;
1021 let table_ident = set.table_ident_sql();
1022 let mut repaired = Vec::new();
1023 for migration in migrations {
1024 let catalog = introspect_catalog_libsql(conn).await?;
1025 let plan =
1026 drizzle_migrations::repair::plan(drizzle_types::Dialect::SQLite, migration, &catalog);
1027 for statement in plan
1028 .into_executable(&table_ident)
1029 .map_err(migrator_cli_error)?
1030 {
1031 conn.execute(&statement, ())
1032 .await
1033 .map_err(|e| CliError::MigrationError(format!("Repair statement failed: {e}")))?;
1034 }
1035 conn.execute(&set.record_migration_finished_sql(migration), ())
1036 .await
1037 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1038 repaired.push(migration.tag().to_string());
1039 }
1040 Ok(repaired)
1041}
1042
1043#[cfg(feature = "postgres-sync")]
1044fn introspect_catalog_postgres_sync(
1045 client: &mut postgres::Client,
1046) -> Result<drizzle_migrations::repair::Catalog, CliError> {
1047 use drizzle_migrations::repair::postgres as pg;
1048
1049 let err = |e: postgres::Error| CliError::MigrationError(e.to_string());
1050 let two =
1051 |client: &mut postgres::Client, sql: &str| -> Result<Vec<(String, String)>, CliError> {
1052 Ok(client
1053 .query(sql, &[])
1054 .map_err(err)?
1055 .iter()
1056 .map(|row| (row.get::<_, String>(0), row.get::<_, String>(1)))
1057 .collect())
1058 };
1059 let three = |client: &mut postgres::Client,
1060 sql: &str|
1061 -> Result<Vec<(String, String, String)>, CliError> {
1062 Ok(client
1063 .query(sql, &[])
1064 .map_err(err)?
1065 .iter()
1066 .map(|row| {
1067 (
1068 row.get::<_, String>(0),
1069 row.get::<_, String>(1),
1070 row.get::<_, String>(2),
1071 )
1072 })
1073 .collect())
1074 };
1075
1076 let indexes = client
1077 .query(pg::INDEXES_QUERY, &[])
1078 .map_err(err)?
1079 .iter()
1080 .map(|row| {
1081 (
1082 row.get::<_, String>(0),
1083 row.get::<_, String>(1),
1084 row.get::<_, bool>(2),
1085 )
1086 })
1087 .collect::<Vec<_>>();
1088
1089 let tables = two(client, pg::TABLES_QUERY)?;
1090 let columns = three(client, pg::COLUMNS_QUERY)?;
1091 let views = two(client, pg::VIEWS_QUERY)?;
1092 let index_columns = three(client, pg::INDEX_COLUMNS_QUERY)?;
1093 let enums = three(client, pg::ENUMS_QUERY)?;
1094
1095 Ok(pg::catalog(
1096 &tables,
1097 &columns,
1098 &views,
1099 &indexes,
1100 &index_columns,
1101 &enums,
1102 ))
1103}
1104
1105#[cfg(feature = "postgres-sync")]
1106fn repair_dirty_migrations_postgres_sync(
1107 client: &mut postgres::Client,
1108 set: &Migrations,
1109 repair: bool,
1110) -> Result<Vec<String>, CliError> {
1111 let dirty = client
1112 .query(&set.dirty_names_sql(), &[])
1113 .map_err(|e| CliError::MigrationError(e.to_string()))?
1114 .iter()
1115 .map(|row| row.try_get::<_, String>(0))
1116 .collect::<Result<Vec<_>, _>>()
1117 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1118
1119 let table_ident = set.table_ident_sql();
1120 let to_repair = dirty_migrations_to_repair(set, &dirty, repair)?
1121 .into_iter()
1122 .cloned()
1123 .collect::<Vec<_>>();
1124
1125 let mut repaired = Vec::new();
1126 for migration in &to_repair {
1127 let catalog = introspect_catalog_postgres_sync(client)?;
1128 let plan = drizzle_migrations::repair::plan(
1129 drizzle_types::Dialect::PostgreSQL,
1130 migration,
1131 &catalog,
1132 );
1133 for statement in plan
1134 .into_executable(&table_ident)
1135 .map_err(migrator_cli_error)?
1136 {
1137 client
1138 .execute(statement.as_str(), &[])
1139 .map_err(|e| CliError::MigrationError(format!("Repair statement failed: {e}")))?;
1140 }
1141 client
1142 .execute(&set.record_migration_finished_sql(migration), &[])
1143 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1144 repaired.push(migration.tag().to_string());
1145 }
1146 Ok(repaired)
1147}
1148
1149#[cfg(feature = "tokio-postgres")]
1150async fn introspect_catalog_postgres_async(
1151 client: &tokio_postgres::Client,
1152) -> Result<drizzle_migrations::repair::Catalog, CliError> {
1153 use drizzle_migrations::repair::postgres as pg;
1154
1155 async fn two(
1156 client: &tokio_postgres::Client,
1157 sql: &str,
1158 ) -> Result<Vec<(String, String)>, CliError> {
1159 Ok(client
1160 .query(sql, &[])
1161 .await
1162 .map_err(|e| CliError::MigrationError(e.to_string()))?
1163 .iter()
1164 .map(|row| (row.get::<_, String>(0), row.get::<_, String>(1)))
1165 .collect())
1166 }
1167
1168 async fn three(
1169 client: &tokio_postgres::Client,
1170 sql: &str,
1171 ) -> Result<Vec<(String, String, String)>, CliError> {
1172 Ok(client
1173 .query(sql, &[])
1174 .await
1175 .map_err(|e| CliError::MigrationError(e.to_string()))?
1176 .iter()
1177 .map(|row| {
1178 (
1179 row.get::<_, String>(0),
1180 row.get::<_, String>(1),
1181 row.get::<_, String>(2),
1182 )
1183 })
1184 .collect())
1185 }
1186
1187 let indexes = client
1188 .query(pg::INDEXES_QUERY, &[])
1189 .await
1190 .map_err(|e| CliError::MigrationError(e.to_string()))?
1191 .iter()
1192 .map(|row| {
1193 (
1194 row.get::<_, String>(0),
1195 row.get::<_, String>(1),
1196 row.get::<_, bool>(2),
1197 )
1198 })
1199 .collect::<Vec<_>>();
1200
1201 let tables = two(client, pg::TABLES_QUERY).await?;
1202 let columns = three(client, pg::COLUMNS_QUERY).await?;
1203 let views = two(client, pg::VIEWS_QUERY).await?;
1204 let index_columns = three(client, pg::INDEX_COLUMNS_QUERY).await?;
1205 let enums = three(client, pg::ENUMS_QUERY).await?;
1206
1207 Ok(pg::catalog(
1208 &tables,
1209 &columns,
1210 &views,
1211 &indexes,
1212 &index_columns,
1213 &enums,
1214 ))
1215}
1216
1217#[cfg(feature = "tokio-postgres")]
1218async fn repair_dirty_migrations_postgres_async(
1219 client: &tokio_postgres::Client,
1220 set: &Migrations,
1221 repair: bool,
1222) -> Result<Vec<String>, CliError> {
1223 let dirty = client
1224 .query(&set.dirty_names_sql(), &[])
1225 .await
1226 .map_err(|e| CliError::MigrationError(e.to_string()))?
1227 .iter()
1228 .map(|row| row.try_get::<_, String>(0))
1229 .collect::<Result<Vec<_>, _>>()
1230 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1231
1232 let table_ident = set.table_ident_sql();
1233 let to_repair = dirty_migrations_to_repair(set, &dirty, repair)?
1234 .into_iter()
1235 .cloned()
1236 .collect::<Vec<_>>();
1237
1238 let mut repaired = Vec::new();
1239 for migration in &to_repair {
1240 let catalog = introspect_catalog_postgres_async(client).await?;
1241 let plan = drizzle_migrations::repair::plan(
1242 drizzle_types::Dialect::PostgreSQL,
1243 migration,
1244 &catalog,
1245 );
1246 for statement in plan
1247 .into_executable(&table_ident)
1248 .map_err(migrator_cli_error)?
1249 {
1250 client
1251 .execute(statement.as_str(), &[])
1252 .await
1253 .map_err(|e| CliError::MigrationError(format!("Repair statement failed: {e}")))?;
1254 }
1255 client
1256 .execute(&set.record_migration_finished_sql(migration), &[])
1257 .await
1258 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1259 repaired.push(migration.tag().to_string());
1260 }
1261 Ok(repaired)
1262}
1263
1264#[cfg(feature = "rusqlite")]
1266fn read_sqlite_tracking_state(
1267 conn: &rusqlite::Connection,
1268 set: &Migrations,
1269) -> Result<TrackingState, CliError> {
1270 let pragma_sql = format!(
1271 "SELECT name FROM pragma_table_info('{}')",
1272 escape_sql_literal(set.table_name())
1273 );
1274 let mut stmt = conn
1275 .prepare(&pragma_sql)
1276 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1277 let columns = stmt
1278 .query_map([], |row| row.get::<_, String>(0))
1279 .map_err(|e| CliError::MigrationError(e.to_string()))?
1280 .collect::<Result<Vec<_>, _>>()
1281 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1282 if columns.is_empty() {
1283 return Ok(TrackingState::Missing);
1284 }
1285 if columns.iter().any(|column| column == "name") {
1286 return Ok(TrackingState::Current);
1287 }
1288
1289 let mut stmt = conn
1290 .prepare(&format!(
1291 "SELECT id, hash, created_at FROM {} ORDER BY id ASC",
1292 set.table_ident_sql()
1293 ))
1294 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1295 let rows = stmt
1296 .query_map([], |row| {
1297 Ok(drizzle_migrations::AppliedMigrationMetadata {
1298 id: row.get::<_, Option<i64>>(0)?,
1299 hash: row.get::<_, String>(1)?,
1300 created_at: row.get::<_, i64>(2)?,
1301 })
1302 })
1303 .map_err(|e| CliError::MigrationError(e.to_string()))?
1304 .collect::<Result<Vec<_>, _>>()
1305 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1306 Ok(TrackingState::Legacy(rows))
1307}
1308
1309#[cfg(feature = "rusqlite")]
1310fn ensure_sqlite_tracking_table(
1311 conn: &rusqlite::Connection,
1312 set: &Migrations,
1313) -> Result<(), CliError> {
1314 conn.execute(&set.create_table_sql(), [])
1315 .map_err(|e| CliError::MigrationError(format!("Failed to create migrations table: {e}")))?;
1316
1317 let TrackingState::Legacy(applied) = read_sqlite_tracking_state(conn, set)? else {
1318 return Ok(());
1319 };
1320
1321 let matched = drizzle_migrations::match_applied_migration_metadata(set.all(), &applied)
1322 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1323
1324 conn.execute("BEGIN", [])
1325 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1326 let result = (|| -> Result<(), CliError> {
1327 conn.execute(
1328 &format!(
1329 "ALTER TABLE {} ADD COLUMN \"name\" text",
1330 set.table_ident_sql()
1331 ),
1332 [],
1333 )
1334 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1335 conn.execute(
1336 &format!(
1337 "ALTER TABLE {} ADD COLUMN \"applied_at\" TEXT",
1338 set.table_ident_sql()
1339 ),
1340 [],
1341 )
1342 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1343
1344 for row in matched {
1345 conn.execute(&set.backfill_migration_metadata_sql(&row), [])
1346 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1347 }
1348
1349 Ok(())
1350 })();
1351
1352 match result {
1353 Ok(()) => {
1354 conn.execute("COMMIT", [])
1355 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1356 Ok(())
1357 }
1358 Err(err) => {
1359 let _ = conn.execute("ROLLBACK", []);
1360 Err(err)
1361 }
1362 }
1363}
1364
1365#[cfg(any(feature = "libsql", feature = "turso"))]
1367async fn read_sqlite_tracking_state_libsql(
1368 conn: &libsql::Connection,
1369 set: &Migrations,
1370) -> Result<TrackingState, CliError> {
1371 let pragma_sql = format!(
1372 "SELECT name FROM pragma_table_info('{}')",
1373 escape_sql_literal(set.table_name())
1374 );
1375 let mut rows = conn
1376 .query(&pragma_sql, ())
1377 .await
1378 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1379 let mut any_column = false;
1380 let mut has_name = false;
1381 while let Some(row) = rows
1382 .next()
1383 .await
1384 .map_err(|e| CliError::MigrationError(e.to_string()))?
1385 {
1386 any_column = true;
1387 let name = row
1388 .get::<String>(0)
1389 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1390 if name == "name" {
1391 has_name = true;
1392 }
1393 }
1394 if !any_column {
1395 return Ok(TrackingState::Missing);
1396 }
1397 if has_name {
1398 return Ok(TrackingState::Current);
1399 }
1400
1401 let mut rows = conn
1402 .query(
1403 &format!(
1404 "SELECT id, hash, created_at FROM {} ORDER BY id ASC",
1405 set.table_ident_sql()
1406 ),
1407 (),
1408 )
1409 .await
1410 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1411 let mut applied = Vec::new();
1412 while let Some(row) = rows
1413 .next()
1414 .await
1415 .map_err(|e| CliError::MigrationError(e.to_string()))?
1416 {
1417 applied.push(drizzle_migrations::AppliedMigrationMetadata {
1418 id: row
1419 .get::<Option<i64>>(0)
1420 .map_err(|e| CliError::MigrationError(e.to_string()))?,
1421 hash: row
1422 .get::<String>(1)
1423 .map_err(|e| CliError::MigrationError(e.to_string()))?,
1424 created_at: row
1425 .get::<i64>(2)
1426 .map_err(|e| CliError::MigrationError(e.to_string()))?,
1427 });
1428 }
1429 Ok(TrackingState::Legacy(applied))
1430}
1431
1432#[cfg(any(feature = "libsql", feature = "turso"))]
1433async fn ensure_sqlite_tracking_table_libsql(
1434 conn: &libsql::Connection,
1435 set: &Migrations,
1436) -> Result<(), CliError> {
1437 conn.execute(&set.create_table_sql(), ())
1438 .await
1439 .map_err(|e| CliError::MigrationError(format!("Failed to create migrations table: {e}")))?;
1440
1441 let TrackingState::Legacy(applied) = read_sqlite_tracking_state_libsql(conn, set).await? else {
1442 return Ok(());
1443 };
1444
1445 let matched = drizzle_migrations::match_applied_migration_metadata(set.all(), &applied)
1446 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1447
1448 let tx = conn
1449 .transaction()
1450 .await
1451 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1452 tx.execute(
1453 &format!(
1454 "ALTER TABLE {} ADD COLUMN \"name\" text",
1455 set.table_ident_sql()
1456 ),
1457 (),
1458 )
1459 .await
1460 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1461 tx.execute(
1462 &format!(
1463 "ALTER TABLE {} ADD COLUMN \"applied_at\" TEXT",
1464 set.table_ident_sql()
1465 ),
1466 (),
1467 )
1468 .await
1469 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1470
1471 for row in matched {
1472 tx.execute(&set.backfill_migration_metadata_sql(&row), ())
1473 .await
1474 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1475 }
1476
1477 tx.commit()
1478 .await
1479 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1480 Ok(())
1481}
1482
1483#[cfg(feature = "postgres-sync")]
1485fn read_postgres_tracking_state_sync(
1486 client: &mut postgres::Client,
1487 set: &Migrations,
1488) -> Result<TrackingState, CliError> {
1489 let schema = set.schema_name().unwrap_or("public");
1490 let rows = client
1491 .query(
1492 "SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2",
1493 &[&schema, &set.table_name()],
1494 )
1495 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1496 let columns = rows
1497 .iter()
1498 .map(|row| row.try_get::<_, String>(0))
1499 .collect::<Result<Vec<_>, postgres::Error>>()
1500 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1501 if columns.is_empty() {
1502 return Ok(TrackingState::Missing);
1503 }
1504 if has_current_tracking_columns(&columns) {
1507 return Ok(TrackingState::Current);
1508 }
1509
1510 let rows = client
1511 .query(
1512 &format!(
1513 "SELECT id::bigint, hash, created_at FROM {} ORDER BY id ASC",
1514 set.table_ident_sql()
1515 ),
1516 &[],
1517 )
1518 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1519 let applied = rows
1520 .iter()
1521 .map(|row| {
1522 Ok(drizzle_migrations::AppliedMigrationMetadata {
1523 id: row.try_get::<_, Option<i64>>(0)?,
1524 hash: row.try_get::<_, String>(1)?,
1525 created_at: row.try_get::<_, i64>(2)?,
1526 })
1527 })
1528 .collect::<Result<Vec<_>, postgres::Error>>()
1529 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1530 Ok(TrackingState::Legacy(applied))
1531}
1532
1533#[cfg(feature = "postgres-sync")]
1534fn ensure_postgres_tracking_table_sync(
1535 client: &mut postgres::Client,
1536 set: &Migrations,
1537) -> Result<(), CliError> {
1538 client
1539 .execute(&set.create_table_sql(), &[])
1540 .map_err(|e| CliError::MigrationError(format!("Failed to create migrations table: {e}")))?;
1541
1542 let TrackingState::Legacy(applied) = read_postgres_tracking_state_sync(client, set)? else {
1543 return Ok(());
1544 };
1545
1546 let matched = drizzle_migrations::match_applied_migration_metadata(set.all(), &applied)
1547 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1548 let (has_name, has_applied_at) = postgres_tracking_columns_sync(client, set)?;
1549
1550 client
1553 .batch_execute(
1554 &set.tracking_upgrade_sql(has_name, has_applied_at, &matched)
1555 .join(";\n"),
1556 )
1557 .map_err(|e| {
1558 CliError::MigrationError(format!(
1559 "Failed to upgrade the migrations table: {}",
1560 postgres_error_text(&e)
1561 ))
1562 })?;
1563
1564 Ok(())
1565}
1566
1567#[cfg(feature = "postgres-sync")]
1569fn postgres_tracking_columns_sync(
1570 client: &mut postgres::Client,
1571 set: &Migrations,
1572) -> Result<(bool, bool), CliError> {
1573 let schema = set.schema_name().unwrap_or("public");
1574 let columns = client
1575 .query(
1576 "SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2",
1577 &[&schema, &set.table_name()],
1578 )
1579 .map_err(|e| CliError::MigrationError(e.to_string()))?
1580 .iter()
1581 .map(|row| row.try_get::<_, String>(0))
1582 .collect::<Result<Vec<_>, postgres::Error>>()
1583 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1584 Ok((
1585 columns.iter().any(|column| column == "name"),
1586 columns.iter().any(|column| column == "applied_at"),
1587 ))
1588}
1589
1590#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
1592fn has_current_tracking_columns(columns: &[String]) -> bool {
1593 columns.iter().any(|column| column == "name")
1594 && columns.iter().any(|column| column == "applied_at")
1595}
1596
1597#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
1600fn postgres_error_text(error: &(dyn std::error::Error + 'static)) -> String {
1601 match error.source() {
1602 Some(source) => format!("{error}: {source}"),
1603 None => error.to_string(),
1604 }
1605}
1606
1607#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
1610fn migration_statement_error(
1611 migration: &drizzle_migrations::Migration,
1612 statement: &str,
1613 error: &(dyn std::error::Error + 'static),
1614) -> CliError {
1615 CliError::MigrationError(format!(
1616 "Migration '{}' failed: {}\n statement: {}",
1617 migration.tag(),
1618 postgres_error_text(error),
1619 statement.trim()
1620 ))
1621}
1622
1623#[cfg(feature = "tokio-postgres")]
1625async fn read_postgres_tracking_state_async(
1626 client: &tokio_postgres::Client,
1627 set: &Migrations,
1628) -> Result<TrackingState, CliError> {
1629 let schema = set.schema_name().unwrap_or("public");
1630 let rows = client
1631 .query(
1632 "SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2",
1633 &[&schema, &set.table_name()],
1634 )
1635 .await
1636 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1637 let columns = rows
1638 .iter()
1639 .map(|row| row.try_get::<_, String>(0))
1640 .collect::<Result<Vec<_>, tokio_postgres::Error>>()
1641 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1642 if columns.is_empty() {
1643 return Ok(TrackingState::Missing);
1644 }
1645 if has_current_tracking_columns(&columns) {
1648 return Ok(TrackingState::Current);
1649 }
1650
1651 let rows = client
1652 .query(
1653 &format!(
1654 "SELECT id::bigint, hash, created_at FROM {} ORDER BY id ASC",
1655 set.table_ident_sql()
1656 ),
1657 &[],
1658 )
1659 .await
1660 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1661 let applied = rows
1662 .iter()
1663 .map(|row| {
1664 Ok(drizzle_migrations::AppliedMigrationMetadata {
1665 id: row.try_get::<_, Option<i64>>(0)?,
1666 hash: row.try_get::<_, String>(1)?,
1667 created_at: row.try_get::<_, i64>(2)?,
1668 })
1669 })
1670 .collect::<Result<Vec<_>, tokio_postgres::Error>>()
1671 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1672 Ok(TrackingState::Legacy(applied))
1673}
1674
1675#[cfg(feature = "tokio-postgres")]
1676async fn ensure_postgres_tracking_table_async(
1677 client: &tokio_postgres::Client,
1678 set: &Migrations,
1679) -> Result<(), CliError> {
1680 client
1681 .execute(&set.create_table_sql(), &[])
1682 .await
1683 .map_err(|e| CliError::MigrationError(format!("Failed to create migrations table: {e}")))?;
1684
1685 let TrackingState::Legacy(applied) = read_postgres_tracking_state_async(client, set).await?
1686 else {
1687 return Ok(());
1688 };
1689
1690 let matched = drizzle_migrations::match_applied_migration_metadata(set.all(), &applied)
1691 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1692 let schema = set.schema_name().unwrap_or("public");
1693 let columns = client
1694 .query(
1695 "SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2",
1696 &[&schema, &set.table_name()],
1697 )
1698 .await
1699 .map_err(|e| CliError::MigrationError(e.to_string()))?
1700 .iter()
1701 .map(|row| row.try_get::<_, String>(0))
1702 .collect::<Result<Vec<_>, tokio_postgres::Error>>()
1703 .map_err(|e| CliError::MigrationError(e.to_string()))?;
1704 let has_name = columns.iter().any(|column| column == "name");
1705 let has_applied_at = columns.iter().any(|column| column == "applied_at");
1706
1707 client
1710 .batch_execute(
1711 &set.tracking_upgrade_sql(has_name, has_applied_at, &matched)
1712 .join(";\n"),
1713 )
1714 .await
1715 .map_err(|e| {
1716 CliError::MigrationError(format!(
1717 "Failed to upgrade the migrations table: {}",
1718 postgres_error_text(&e)
1719 ))
1720 })?;
1721
1722 Ok(())
1723}
1724
1725fn push_requires_confirmation(
1728 dialect: Dialect,
1729 statements: &[String],
1730 warnings: &[String],
1731 lost: &[(drizzle_migrations::DataLoss, Option<i64>)],
1732) -> bool {
1733 let holds_data = lost
1734 .iter()
1735 .any(|(_, rows)| rows.is_none_or(|rows| rows > 0));
1736 let dropped_columns = lost
1739 .iter()
1740 .filter(|(drop, _)| drop.column.is_some())
1741 .count();
1742 let column_drops: usize = statements
1743 .iter()
1744 .map(|statement| {
1745 statement
1746 .to_ascii_uppercase()
1747 .matches("DROP COLUMN")
1748 .count()
1749 })
1750 .sum();
1751 holds_data
1752 || column_drops > dropped_columns
1753 || statements
1754 .iter()
1755 .any(|statement| statement.to_ascii_uppercase().contains("TRUNCATE"))
1756 || (dialect == Dialect::Mysql && !warnings.is_empty())
1762}
1763
1764fn count_data_loss(
1768 connection: &ResolvedConnection,
1769 data_loss: Vec<drizzle_migrations::DataLoss>,
1770) -> Result<Vec<(drizzle_migrations::DataLoss, Option<i64>)>, CliError> {
1771 if data_loss.is_empty() {
1772 return Ok(Vec::new());
1773 }
1774 let dialect = connection.dialect.to_base();
1775 let queries: Vec<String> = data_loss
1776 .iter()
1777 .map(|drop| drop.count_sql(dialect))
1778 .collect();
1779 let counts = query_counts(connection, &queries)?;
1780 Ok(match counts {
1781 Some(counts) => data_loss
1782 .into_iter()
1783 .zip(counts.into_iter().map(Some))
1784 .collect(),
1785 None => data_loss.into_iter().map(|drop| (drop, None)).collect(),
1786 })
1787}
1788
1789#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
1790fn is_postgres_concurrent_index_statement(sql: &str) -> bool {
1791 drizzle_migrations::is_postgres_concurrent_index_statement(sql)
1792}
1793
1794#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
1795fn has_postgres_concurrent_index(statements: &[String]) -> bool {
1796 statements
1797 .iter()
1798 .any(|stmt| is_postgres_concurrent_index_statement(stmt))
1799}
1800
1801fn confirm_destructive() -> Result<bool, CliError> {
1802 use std::io::{self, IsTerminal, Write};
1803
1804 if !io::stdin().is_terminal() {
1805 return Err(CliError::Other(
1806 "Refusing to apply potentially destructive changes in non-interactive mode. Use --explain or --force."
1807 .into(),
1808 ));
1809 }
1810
1811 println!(
1812 "{}",
1813 output::warning("This push loses data (see the warnings above).")
1814 );
1815 print!("Apply anyway? [y/N]: ");
1816 io::stdout()
1817 .flush()
1818 .map_err(|e| CliError::IoError(e.to_string()))?;
1819
1820 let mut line = String::new();
1821 io::stdin()
1822 .read_line(&mut line)
1823 .map_err(|e| CliError::IoError(e.to_string()))?;
1824 let ans = line.trim().to_ascii_lowercase();
1825 Ok(ans == "y" || ans == "yes")
1826}
1827
1828fn generate_push_sql(
1829 current: &Snapshot,
1830 desired: &Snapshot,
1831 _breakpoints: bool,
1832 options: &drizzle_migrations::DiffOptions,
1833) -> Result<drizzle_migrations::Plan, CliError> {
1834 drizzle_migrations::diff_with(current, desired, options)
1835 .map_err(|error| CliError::MigrationError(error.to_string()))
1836}
1837
1838fn execute_statements(
1839 connection: &ResolvedConnection,
1840 statements: &[String],
1841) -> Result<(), CliError> {
1842 let _ = statements;
1845
1846 match &connection.credentials {
1847 #[cfg(feature = "rusqlite")]
1848 Credentials::Sqlite { path } if connection.driver == Driver::Rusqlite => {
1849 execute_sqlite_statements(path, statements)
1850 }
1851
1852 #[cfg(feature = "rusqlite")]
1853 Credentials::Sqlite { .. } => Err(connection_driver_mismatch(connection)),
1854
1855 #[cfg(not(feature = "rusqlite"))]
1856 Credentials::Sqlite { .. } => Err(CliError::MissingDriver {
1857 dialect: "SQLite",
1858 feature: "rusqlite",
1859 }),
1860
1861 #[cfg(any(feature = "libsql", feature = "turso"))]
1862 Credentials::Turso { url, auth_token } => {
1863 if connection.driver == Driver::Libsql {
1864 #[cfg(feature = "libsql")]
1865 {
1866 execute_libsql_local_statements(url, statements)
1867 }
1868 #[cfg(not(feature = "libsql"))]
1869 {
1870 Err(CliError::MissingDriver {
1871 dialect: "LibSQL (local)",
1872 feature: "libsql",
1873 })
1874 }
1875 } else if connection.driver == Driver::Turso {
1876 #[cfg(feature = "turso")]
1877 {
1878 execute_turso_statements(url, auth_token.as_deref(), statements)
1879 }
1880 #[cfg(not(feature = "turso"))]
1881 {
1882 let _ = auth_token;
1883 Err(CliError::MissingDriver {
1884 dialect: "Turso (remote)",
1885 feature: "turso",
1886 })
1887 }
1888 } else {
1889 Err(connection_driver_mismatch(connection))
1890 }
1891 }
1892
1893 #[cfg(all(not(feature = "turso"), not(feature = "libsql")))]
1894 Credentials::Turso { .. } => Err(CliError::MissingDriver {
1895 dialect: "Turso",
1896 feature: "turso or libsql",
1897 }),
1898
1899 Credentials::Postgres(creds) => {
1900 let _ = creds;
1901 match connection.driver {
1902 Driver::PostgresSync => {
1903 #[cfg(feature = "postgres-sync")]
1904 {
1905 execute_postgres_sync_statements(creds, statements)
1906 }
1907 #[cfg(not(feature = "postgres-sync"))]
1908 {
1909 Err(CliError::MissingDriver {
1910 dialect: "PostgreSQL",
1911 feature: "postgres-sync",
1912 })
1913 }
1914 }
1915 Driver::TokioPostgres => {
1916 #[cfg(feature = "tokio-postgres")]
1917 {
1918 execute_postgres_async_statements(creds, statements)
1919 }
1920 #[cfg(not(feature = "tokio-postgres"))]
1921 {
1922 Err(CliError::MissingDriver {
1923 dialect: "PostgreSQL",
1924 feature: "tokio-postgres",
1925 })
1926 }
1927 }
1928 _ => Err(connection_driver_mismatch(connection)),
1929 }
1930 }
1931
1932 Credentials::MySQL(creds) => {
1933 mysql::execute_statements(connection.driver, creds, statements)
1934 }
1935
1936 #[cfg(feature = "d1-http")]
1937 Credentials::D1 {
1938 account_id,
1939 database_id,
1940 token,
1941 } if connection.driver == Driver::D1Http => {
1942 d1_http::execute_statements(account_id, database_id, token, statements)
1943 }
1944
1945 #[cfg(feature = "d1-http")]
1946 Credentials::D1 { .. } => Err(connection_driver_mismatch(connection)),
1947
1948 #[cfg(not(feature = "d1-http"))]
1949 Credentials::D1 { .. } => Err(CliError::MissingDriver {
1950 dialect: "Cloudflare D1 (HTTP)",
1951 feature: "d1-http",
1952 }),
1953
1954 Credentials::AwsDataApi { .. } => Err(CliError::UnsupportedForDriver {
1955 operation: "Direct SQL execution against AWS Data API",
1956 driver: "aws-data-api",
1957 hint: "Use `aws rds-data execute-statement --sql=\"...\"` with the matching \
1958 --resource-arn / --secret-arn / --database, or connect directly via \
1959 tokio-postgres.",
1960 }),
1961 }
1962}
1963
1964fn query_counts(
1969 connection: &ResolvedConnection,
1970 queries: &[String],
1971) -> Result<Option<Vec<i64>>, CliError> {
1972 let _ = queries;
1975
1976 match &connection.credentials {
1977 #[cfg(feature = "rusqlite")]
1978 Credentials::Sqlite { path } if connection.driver == Driver::Rusqlite => {
1979 query_sqlite_counts(path, queries).map(Some)
1980 }
1981
1982 #[cfg(feature = "rusqlite")]
1983 Credentials::Sqlite { .. } => Err(connection_driver_mismatch(connection)),
1984
1985 #[cfg(not(feature = "rusqlite"))]
1986 Credentials::Sqlite { .. } => Err(CliError::MissingDriver {
1987 dialect: "SQLite",
1988 feature: "rusqlite",
1989 }),
1990
1991 #[cfg(any(feature = "libsql", feature = "turso"))]
1992 Credentials::Turso { url, auth_token } => {
1993 if connection.driver == Driver::Libsql {
1994 #[cfg(feature = "libsql")]
1995 {
1996 query_libsql_local_counts(url, queries).map(Some)
1997 }
1998 #[cfg(not(feature = "libsql"))]
1999 {
2000 Err(CliError::MissingDriver {
2001 dialect: "LibSQL (local)",
2002 feature: "libsql",
2003 })
2004 }
2005 } else if connection.driver == Driver::Turso {
2006 #[cfg(feature = "turso")]
2007 {
2008 query_turso_counts(url, auth_token.as_deref(), queries).map(Some)
2009 }
2010 #[cfg(not(feature = "turso"))]
2011 {
2012 let _ = auth_token;
2013 Err(CliError::MissingDriver {
2014 dialect: "Turso (remote)",
2015 feature: "turso",
2016 })
2017 }
2018 } else {
2019 Err(connection_driver_mismatch(connection))
2020 }
2021 }
2022
2023 #[cfg(all(not(feature = "turso"), not(feature = "libsql")))]
2024 Credentials::Turso { .. } => Err(CliError::MissingDriver {
2025 dialect: "Turso",
2026 feature: "turso or libsql",
2027 }),
2028
2029 Credentials::Postgres(creds) => {
2030 let _ = creds;
2031 match connection.driver {
2032 Driver::PostgresSync => {
2033 #[cfg(feature = "postgres-sync")]
2034 {
2035 query_postgres_sync_counts(creds, queries).map(Some)
2036 }
2037 #[cfg(not(feature = "postgres-sync"))]
2038 {
2039 Err(CliError::MissingDriver {
2040 dialect: "PostgreSQL",
2041 feature: "postgres-sync",
2042 })
2043 }
2044 }
2045 Driver::TokioPostgres => {
2046 #[cfg(feature = "tokio-postgres")]
2047 {
2048 query_postgres_async_counts(creds, queries).map(Some)
2049 }
2050 #[cfg(not(feature = "tokio-postgres"))]
2051 {
2052 Err(CliError::MissingDriver {
2053 dialect: "PostgreSQL",
2054 feature: "tokio-postgres",
2055 })
2056 }
2057 }
2058 _ => Err(connection_driver_mismatch(connection)),
2059 }
2060 }
2061
2062 Credentials::MySQL(creds) => {
2063 mysql::query_counts(connection.driver, creds, queries).map(Some)
2064 }
2065
2066 #[cfg(feature = "d1-http")]
2067 Credentials::D1 {
2068 account_id,
2069 database_id,
2070 token,
2071 } if connection.driver == Driver::D1Http => {
2072 d1_http::query_counts(account_id, database_id, token, queries).map(Some)
2073 }
2074
2075 #[cfg(feature = "d1-http")]
2076 Credentials::D1 { .. } => Err(connection_driver_mismatch(connection)),
2077
2078 #[cfg(not(feature = "d1-http"))]
2079 Credentials::D1 { .. } => Err(CliError::MissingDriver {
2080 dialect: "Cloudflare D1 (HTTP)",
2081 feature: "d1-http",
2082 }),
2083
2084 Credentials::AwsDataApi { .. } => Ok(None),
2085 }
2086}
2087
2088#[allow(dead_code)] fn count_query_error(query: &str, error: impl std::fmt::Display) -> CliError {
2091 CliError::MigrationError(format!(
2092 "Counting data before push failed: {error}\n{query}"
2093 ))
2094}
2095
2096#[allow(dead_code)]
2098fn is_local_libsql(url: &str) -> bool {
2099 url.starts_with("file:")
2100 || url.starts_with("./")
2101 || url.starts_with('/')
2102 || !url.contains("://")
2103}
2104
2105#[cfg(feature = "rusqlite")]
2110fn execute_sqlite_statements(path: &str, statements: &[String]) -> Result<(), CliError> {
2111 let conn = rusqlite::Connection::open(path).map_err(|e| {
2112 CliError::ConnectionError(format!("Failed to open SQLite database '{path}': {e}"))
2113 })?;
2114
2115 execute_sqlite_statements_on_connection(&conn, statements)
2116}
2117
2118#[cfg(feature = "rusqlite")]
2119fn query_sqlite_counts(path: &str, queries: &[String]) -> Result<Vec<i64>, CliError> {
2120 let conn = rusqlite::Connection::open(path).map_err(|e| {
2121 CliError::ConnectionError(format!("Failed to open SQLite database '{path}': {e}"))
2122 })?;
2123 queries
2124 .iter()
2125 .map(|query| {
2126 conn.query_row(query, [], |row| row.get::<_, i64>(0))
2127 .map_err(|error| count_query_error(query, error))
2128 })
2129 .collect()
2130}
2131
2132#[cfg(feature = "rusqlite")]
2133fn execute_sqlite_statements_on_connection(
2134 conn: &rusqlite::Connection,
2135 statements: &[String],
2136) -> Result<(), CliError> {
2137 let operation =
2138 drizzle_migrations::Migration::with_hash("sqlite_push", "", 0, statements.to_vec());
2139 let execution = operation
2140 .sqlite_execution()
2141 .map_err(sqlite_execution_cli_error)?;
2142 let foreign_keys_were_enabled = sqlite_foreign_keys_enabled_cli(conn)?;
2143 let suspend_foreign_keys = execution.suspends_foreign_keys() && foreign_keys_were_enabled;
2144 if suspend_foreign_keys && let Err(error) = set_sqlite_foreign_keys_cli(conn, false) {
2145 let restore = set_sqlite_foreign_keys_cli(conn, true);
2146 return finish_sqlite_foreign_key_scope(Err(error), restore);
2147 }
2148
2149 let result = (|| -> Result<(), CliError> {
2150 conn.execute("BEGIN IMMEDIATE", [])
2151 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2152 for statement in execution
2153 .statements()
2154 .filter(|statement| !statement.trim().is_empty())
2155 {
2156 conn.execute(statement, []).map_err(|error| {
2157 CliError::MigrationError(format!("Statement failed: {error}\n{statement}"))
2158 })?;
2159 }
2160 if execution.suspends_foreign_keys() {
2161 verify_sqlite_foreign_keys_cli(conn)?;
2162 }
2163 conn.execute("COMMIT", [])
2164 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2165 Ok(())
2166 })();
2167 let result = match result {
2168 Ok(()) => Ok(()),
2169 Err(error) => {
2170 let _ = conn.execute("ROLLBACK", []);
2171 Err(error)
2172 }
2173 };
2174 let restore = if suspend_foreign_keys {
2175 set_sqlite_foreign_keys_cli(conn, true)
2176 } else {
2177 Ok(())
2178 };
2179 finish_sqlite_foreign_key_scope(result, restore)
2180}
2181
2182#[cfg(feature = "rusqlite")]
2183fn run_sqlite_migrations(
2184 set: &Migrations,
2185 path: &str,
2186 repair: bool,
2187) -> Result<MigrationResult, CliError> {
2188 let conn = rusqlite::Connection::open(path).map_err(|e| {
2189 CliError::ConnectionError(format!("Failed to open SQLite database '{path}': {e}"))
2190 })?;
2191
2192 run_sqlite_migrations_on_connection(set, &conn, repair)
2193}
2194
2195#[cfg(feature = "rusqlite")]
2196fn run_sqlite_migrations_on_connection(
2197 set: &Migrations,
2198 conn: &rusqlite::Connection,
2199 repair: bool,
2200) -> Result<MigrationResult, CliError> {
2201 ensure_sqlite_tracking_table(conn, set)?;
2202 conn.busy_timeout(std::time::Duration::from_secs(30))
2203 .map_err(|e| CliError::MigrationError(e.to_string()))?;
2204 let applied_before_transaction = query_applied_names_sqlite(conn, set)?;
2205 let suspends_foreign_keys = set
2206 .pending(&applied_before_transaction)
2207 .map(|migration| migration.sqlite_execution())
2208 .collect::<Result<Vec<_>, _>>()
2209 .map_err(sqlite_execution_cli_error)?
2210 .iter()
2211 .any(|execution| execution.suspends_foreign_keys());
2212 let foreign_keys_were_enabled = sqlite_foreign_keys_enabled_cli(conn)?;
2213 let suspend_foreign_keys = suspends_foreign_keys && foreign_keys_were_enabled;
2214 if suspend_foreign_keys && let Err(error) = set_sqlite_foreign_keys_cli(conn, false) {
2215 let restore = set_sqlite_foreign_keys_cli(conn, true);
2216 return finish_sqlite_foreign_key_scope(Err(error), restore);
2217 }
2218
2219 let result = (|| -> Result<MigrationResult, CliError> {
2220 conn.execute("BEGIN IMMEDIATE", [])
2221 .map_err(|e| CliError::MigrationError(e.to_string()))?;
2222 let repaired = repair_dirty_migrations_sqlite(conn, set, repair)?;
2223 let applied_names = query_applied_names_sqlite(conn, set)?;
2224 let pending = set
2225 .pending(&applied_names)
2226 .map(|migration| {
2227 migration
2228 .sqlite_execution()
2229 .map(|execution| (migration, execution))
2230 })
2231 .collect::<Result<Vec<_>, _>>()
2232 .map_err(sqlite_execution_cli_error)?;
2233
2234 let mut applied = Vec::with_capacity(pending.len());
2235 for (migration, execution) in pending {
2236 for statement in execution
2237 .statements()
2238 .filter(|statement| !statement.trim().is_empty())
2239 {
2240 conn.execute(statement, []).map_err(|error| {
2241 CliError::MigrationError(format!(
2242 "Migration '{}' failed: {error}",
2243 migration.hash()
2244 ))
2245 })?;
2246 }
2247 conn.execute(&set.record_migration_sql(migration), [])
2248 .map_err(|e| CliError::MigrationError(e.to_string()))?;
2249 applied.push(migration.hash().to_string());
2250 }
2251
2252 Ok(MigrationResult {
2253 applied_count: applied.len(),
2254 applied_migrations: applied,
2255 repaired_migrations: repaired,
2256 })
2257 })();
2258
2259 let result = match result {
2260 Ok(outcome) => match suspends_foreign_keys
2261 .then(|| verify_sqlite_foreign_keys_cli(conn))
2262 .transpose()
2263 {
2264 Ok(_) => conn
2265 .execute("COMMIT", [])
2266 .map(|_| outcome)
2267 .map_err(|e| CliError::MigrationError(e.to_string())),
2268 Err(error) => {
2269 let _ = conn.execute("ROLLBACK", []);
2270 Err(error)
2271 }
2272 },
2273 Err(error) => {
2274 let _ = conn.execute("ROLLBACK", []);
2275 Err(error)
2276 }
2277 };
2278 let restore = if suspend_foreign_keys {
2279 set_sqlite_foreign_keys_cli(conn, true)
2280 } else {
2281 Ok(())
2282 };
2283 finish_sqlite_foreign_key_scope(result, restore)
2284}
2285
2286#[cfg(feature = "rusqlite")]
2287fn sqlite_foreign_keys_enabled_cli(conn: &rusqlite::Connection) -> Result<bool, CliError> {
2288 conn.query_row("PRAGMA foreign_keys", [], |row| row.get::<_, i64>(0))
2289 .map(|enabled| enabled != 0)
2290 .map_err(|error| CliError::MigrationError(error.to_string()))
2291}
2292
2293#[cfg(feature = "rusqlite")]
2294fn set_sqlite_foreign_keys_cli(conn: &rusqlite::Connection, enabled: bool) -> Result<(), CliError> {
2295 conn.execute_batch(if enabled {
2296 "PRAGMA foreign_keys=ON"
2297 } else {
2298 "PRAGMA foreign_keys=OFF"
2299 })
2300 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2301 if sqlite_foreign_keys_enabled_cli(conn)? != enabled {
2302 return Err(CliError::MigrationError(format!(
2303 "SQLite refused to set foreign_keys={}",
2304 if enabled { "ON" } else { "OFF" }
2305 )));
2306 }
2307 Ok(())
2308}
2309
2310#[cfg(feature = "rusqlite")]
2311fn verify_sqlite_foreign_keys_cli(conn: &rusqlite::Connection) -> Result<(), CliError> {
2312 let mut statement = conn
2313 .prepare("PRAGMA foreign_key_check")
2314 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2315 let mut rows = statement
2316 .query([])
2317 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2318 if rows
2319 .next()
2320 .map_err(|error| CliError::MigrationError(error.to_string()))?
2321 .is_some()
2322 {
2323 return Err(CliError::MigrationError(
2324 "SQLite foreign_key_check failed after migration rebuild".to_string(),
2325 ));
2326 }
2327 Ok(())
2328}
2329
2330#[cfg(feature = "rusqlite")]
2331fn inspect_sqlite_migrations(set: &Migrations, path: &str) -> Result<MigrationPlan, CliError> {
2332 if !sqlite_database_exists(path) {
2333 return build_migration_plan(set, &[]);
2334 }
2335 let conn = rusqlite::Connection::open(path).map_err(|e| {
2336 CliError::ConnectionError(format!("Failed to open SQLite database '{path}': {e}"))
2337 })?;
2338
2339 let applied = match read_sqlite_tracking_state(&conn, set)? {
2340 TrackingState::Missing => Vec::new(),
2341 TrackingState::Legacy(rows) => legacy_applied_records(set, &rows)?,
2342 TrackingState::Current => query_applied_records_sqlite(&conn, set)?,
2343 };
2344 build_migration_plan(set, &applied)
2345}
2346
2347#[cfg(feature = "rusqlite")]
2348fn is_sqlite_missing_table(error: &rusqlite::Error) -> bool {
2349 matches!(
2350 error,
2351 rusqlite::Error::SqliteFailure(_, Some(message)) if message.contains("no such table")
2352 )
2353}
2354
2355#[cfg(feature = "rusqlite")]
2356fn query_applied_records_sqlite(
2357 conn: &rusqlite::Connection,
2358 set: &Migrations,
2359) -> Result<Vec<AppliedMigrationRecord>, CliError> {
2360 let sql = set.applied_records_sql();
2361 let mut stmt = match conn.prepare(&sql) {
2362 Ok(stmt) => stmt,
2363 Err(error) if is_sqlite_missing_table(&error) => return Ok(vec![]),
2364 Err(error) => return Err(CliError::MigrationError(error.to_string())),
2365 };
2366
2367 let mut applied = Vec::new();
2368 let rows = stmt
2369 .query_map([], |row| {
2370 Ok((
2371 row.get::<_, String>(0)?,
2372 row.get::<_, String>(1)?,
2373 row.get::<_, i64>(2)? != 0,
2374 ))
2375 })
2376 .map_err(|e| CliError::MigrationError(e.to_string()))?;
2377
2378 for row in rows {
2379 let (hash, name, dirty) = row.map_err(|e| CliError::MigrationError(e.to_string()))?;
2380 applied.push(AppliedMigrationRecord { hash, name, dirty });
2381 }
2382
2383 Ok(applied)
2384}
2385
2386#[cfg(feature = "rusqlite")]
2387fn query_applied_names_sqlite(
2388 conn: &rusqlite::Connection,
2389 set: &Migrations,
2390) -> Result<Vec<String>, CliError> {
2391 let mut stmt = match conn.prepare(&set.applied_names_sql()) {
2392 Ok(stmt) => stmt,
2393 Err(error) if is_sqlite_missing_table(&error) => return Ok(vec![]),
2394 Err(error) => return Err(CliError::MigrationError(error.to_string())),
2395 };
2396
2397 let names = stmt
2398 .query_map([], |row| row.get::<_, String>(0))
2399 .map_err(|e| CliError::MigrationError(e.to_string()))?
2400 .collect::<Result<Vec<_>, _>>()
2401 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2402
2403 Ok(names)
2404}
2405
2406#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
2411fn postgres_tls_connector(
2412 mode: crate::config::PostgresSslMode,
2413) -> Result<postgres_native_tls::MakeTlsConnector, CliError> {
2414 let mut builder = native_tls::TlsConnector::builder();
2415 if mode == crate::config::PostgresSslMode::VerifyCa {
2416 builder.danger_accept_invalid_hostnames(true);
2417 }
2418 let connector = builder
2419 .build()
2420 .map_err(|error| CliError::ConnectionError(error.to_string()))?;
2421 Ok(postgres_native_tls::MakeTlsConnector::new(connector))
2422}
2423
2424#[cfg(feature = "postgres-sync")]
2425fn connect_postgres_sync(creds: &PostgresCreds) -> Result<postgres::Client, CliError> {
2426 let connection = creds
2427 .connection_config()
2428 .map_err(CliError::ConnectionError)?;
2429 let config = match creds {
2430 PostgresCreds::Url(url) => url
2431 .parse::<postgres::Config>()
2432 .map_err(|error| CliError::ConnectionError(error.to_string()))?,
2433 PostgresCreds::Host {
2434 host,
2435 port,
2436 user,
2437 password,
2438 database,
2439 ..
2440 } => {
2441 let mut config = postgres::Config::new();
2442 config
2443 .host(host.as_ref())
2444 .port(*port)
2445 .dbname(database.as_ref());
2446 if let Some(user) = user {
2447 config.user(user.as_ref());
2448 }
2449 if let Some(password) = password {
2450 config.password(password.as_bytes());
2451 }
2452 config.ssl_mode(connection.config.get_ssl_mode());
2453 config
2454 }
2455 };
2456 if connection.ssl == crate::config::PostgresSslMode::Disable {
2457 config
2458 .connect(postgres::NoTls)
2459 .map_err(|error| CliError::ConnectionError(error.to_string()))
2460 } else {
2461 let connector = postgres_tls_connector(connection.ssl)?;
2462 config
2463 .connect(connector)
2464 .map_err(|error| CliError::ConnectionError(error.to_string()))
2465 }
2466}
2467
2468#[cfg(feature = "tokio-postgres")]
2469async fn connect_postgres_async(creds: &PostgresCreds) -> Result<tokio_postgres::Client, CliError> {
2470 let connection = creds
2471 .connection_config()
2472 .map_err(CliError::ConnectionError)?;
2473 if connection.ssl == crate::config::PostgresSslMode::Disable {
2474 let (client, driver) = connection
2475 .config
2476 .connect(tokio_postgres::NoTls)
2477 .await
2478 .map_err(|error| CliError::ConnectionError(error.to_string()))?;
2479 tokio::spawn(async move {
2480 if let Err(error) = driver.await {
2481 eprintln!("PostgreSQL connection error: {error}");
2482 }
2483 });
2484 Ok(client)
2485 } else {
2486 let connector = postgres_tls_connector(connection.ssl)?;
2487 let (client, driver) = connection
2488 .config
2489 .connect(connector)
2490 .await
2491 .map_err(|error| CliError::ConnectionError(error.to_string()))?;
2492 tokio::spawn(async move {
2493 if let Err(error) = driver.await {
2494 eprintln!("PostgreSQL connection error: {error}");
2495 }
2496 });
2497 Ok(client)
2498 }
2499}
2500
2501#[cfg(feature = "postgres-sync")]
2502fn query_postgres_sync_counts(
2503 creds: &PostgresCreds,
2504 queries: &[String],
2505) -> Result<Vec<i64>, CliError> {
2506 let mut client = connect_postgres_sync(creds)?;
2507 queries
2508 .iter()
2509 .map(|query| {
2510 client
2511 .query_one(query.as_str(), &[])
2512 .and_then(|row| row.try_get::<_, i64>(0))
2513 .map_err(|error| count_query_error(query, postgres_error_text(&error)))
2514 })
2515 .collect()
2516}
2517
2518#[cfg(feature = "postgres-sync")]
2519fn execute_postgres_sync_statements(
2520 creds: &PostgresCreds,
2521 statements: &[String],
2522) -> Result<(), CliError> {
2523 let mut client = connect_postgres_sync(creds)?;
2524
2525 if has_postgres_concurrent_index(statements) {
2526 for stmt in statements {
2528 let s = stmt.trim();
2529 if s.is_empty() {
2530 continue;
2531 }
2532 client
2533 .execute(s, &[])
2534 .map_err(|e| CliError::MigrationError(format!("Statement failed: {e}\n{s}")))?;
2535 }
2536 } else {
2537 let mut tx = client
2538 .transaction()
2539 .map_err(|e| CliError::MigrationError(e.to_string()))?;
2540
2541 for stmt in statements {
2542 let s = stmt.trim();
2543 if s.is_empty() {
2544 continue;
2545 }
2546 tx.execute(s, &[])
2547 .map_err(|e| CliError::MigrationError(format!("Statement failed: {e}\n{s}")))?;
2548 }
2549
2550 tx.commit()
2551 .map_err(|e| CliError::MigrationError(e.to_string()))?;
2552 }
2553
2554 Ok(())
2555}
2556
2557#[cfg(feature = "postgres-sync")]
2558fn run_postgres_sync_migrations(
2559 set: &Migrations,
2560 creds: &PostgresCreds,
2561 repair: bool,
2562) -> Result<MigrationResult, CliError> {
2563 let mut client = connect_postgres_sync(creds)?;
2564
2565 let lock_key = set.postgres_advisory_lock_key();
2568 client
2569 .query_one("SELECT pg_advisory_lock($1)", &[&lock_key])
2570 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2571 let result = run_postgres_sync_migrations_locked(&mut client, set, repair);
2572 let unlock = client
2573 .query_one("SELECT pg_advisory_unlock($1)", &[&lock_key])
2574 .map_err(|error| CliError::MigrationError(error.to_string()));
2575 match (result, unlock) {
2576 (Ok(result), Ok(_)) => Ok(result),
2577 (Err(error), _) => Err(error),
2578 (Ok(_), Err(error)) => Err(error),
2579 }
2580}
2581
2582#[cfg(feature = "postgres-sync")]
2583fn run_postgres_sync_migrations_locked(
2584 client: &mut postgres::Client,
2585 set: &Migrations,
2586 repair: bool,
2587) -> Result<MigrationResult, CliError> {
2588 if let Some(schema_sql) = set.create_schema_sql() {
2589 client
2590 .execute(&schema_sql, &[])
2591 .map_err(|e| CliError::MigrationError(postgres_error_text(&e)))?;
2592 }
2593 ensure_postgres_tracking_table_sync(client, set)?;
2594 let repaired = repair_dirty_migrations_postgres_sync(client, set, repair)?;
2595 let rows = client
2596 .query(&set.applied_names_sql(), &[])
2597 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2598 let applied_names = rows
2599 .iter()
2600 .map(|row| row.try_get(0))
2601 .collect::<Result<Vec<String>, _>>()
2602 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2603 let pending: Vec<_> = set.pending(&applied_names).collect();
2604 if pending.is_empty() {
2605 return Ok(MigrationResult {
2606 applied_count: 0,
2607 applied_migrations: vec![],
2608 repaired_migrations: repaired,
2609 });
2610 }
2611
2612 let mut applied = Vec::new();
2613 if set.has_postgres_concurrent_index() {
2614 for migration in &pending {
2620 client
2621 .execute(&set.record_migration_started_sql(migration), &[])
2622 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2623
2624 let mut executed = 0usize;
2625 for statement in migration.statements() {
2626 if statement.trim().is_empty() {
2627 continue;
2628 }
2629 if let Err(error) = client.batch_execute(statement) {
2630 if executed == 0 {
2633 let _ = client.execute(&set.clear_migration_started_sql(migration), &[]);
2634 }
2635 return Err(migration_statement_error(migration, statement, &error));
2636 }
2637 executed += 1;
2638 }
2639
2640 client
2641 .execute(&set.record_migration_finished_sql(migration), &[])
2642 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2643 applied.push(migration.hash().to_string());
2644 }
2645 } else {
2646 let mut transaction = client
2647 .transaction()
2648 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2649 for migration in &pending {
2650 for statement in migration.statements() {
2651 if !statement.trim().is_empty() {
2652 transaction
2654 .batch_execute(statement)
2655 .map_err(|error| migration_statement_error(migration, statement, &error))?;
2656 }
2657 }
2658 transaction
2659 .execute(&set.record_migration_sql(migration), &[])
2660 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2661 applied.push(migration.hash().to_string());
2662 }
2663 transaction
2664 .commit()
2665 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2666 }
2667
2668 Ok(MigrationResult {
2669 applied_count: applied.len(),
2670 applied_migrations: applied,
2671 repaired_migrations: repaired,
2672 })
2673}
2674
2675#[cfg(feature = "postgres-sync")]
2676fn inspect_postgres_sync_migrations(
2677 set: &Migrations,
2678 creds: &PostgresCreds,
2679) -> Result<MigrationPlan, CliError> {
2680 let mut client = connect_postgres_sync(creds)?;
2681
2682 let applied = match read_postgres_tracking_state_sync(&mut client, set)? {
2684 TrackingState::Missing => Vec::new(),
2685 TrackingState::Legacy(rows) => legacy_applied_records(set, &rows)?,
2686 TrackingState::Current => query_applied_records_postgres_sync(&mut client, set)?,
2687 };
2688 build_migration_plan(set, &applied)
2689}
2690
2691#[cfg(feature = "postgres-sync")]
2692fn query_applied_records_postgres_sync(
2693 client: &mut postgres::Client,
2694 set: &Migrations,
2695) -> Result<Vec<AppliedMigrationRecord>, CliError> {
2696 let rows = client
2697 .query(&set.applied_records_sql(), &[])
2698 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2699
2700 let mut applied = Vec::new();
2701 for row in rows {
2702 let hash = row
2703 .try_get::<_, Option<String>>(0)
2704 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2705 let name = row
2706 .try_get::<_, Option<String>>(1)
2707 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2708 let dirty = row
2709 .try_get::<_, bool>(2)
2710 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2711 if let (Some(hash), Some(name)) = (hash, name) {
2712 applied.push(AppliedMigrationRecord { hash, name, dirty });
2713 }
2714 }
2715
2716 Ok(applied)
2717}
2718
2719#[cfg(feature = "tokio-postgres")]
2724fn query_postgres_async_counts(
2725 creds: &PostgresCreds,
2726 queries: &[String],
2727) -> Result<Vec<i64>, CliError> {
2728 let rt = tokio::runtime::Builder::new_current_thread()
2729 .enable_all()
2730 .build()
2731 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {}", e)))?;
2732
2733 rt.block_on(async {
2734 let client = connect_postgres_async(creds).await?;
2735 let mut counts = Vec::with_capacity(queries.len());
2736 for query in queries {
2737 let count = client
2738 .query_one(query.as_str(), &[])
2739 .await
2740 .and_then(|row| row.try_get::<_, i64>(0))
2741 .map_err(|error| count_query_error(query, postgres_error_text(&error)))?;
2742 counts.push(count);
2743 }
2744 Ok(counts)
2745 })
2746}
2747
2748#[cfg(feature = "tokio-postgres")]
2749fn execute_postgres_async_statements(
2750 creds: &PostgresCreds,
2751 statements: &[String],
2752) -> Result<(), CliError> {
2753 let rt = tokio::runtime::Builder::new_current_thread()
2754 .enable_all()
2755 .build()
2756 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {}", e)))?;
2757
2758 rt.block_on(execute_postgres_async_inner(creds, statements))
2759}
2760
2761#[cfg(feature = "tokio-postgres")]
2762async fn execute_postgres_async_inner(
2763 creds: &PostgresCreds,
2764 statements: &[String],
2765) -> Result<(), CliError> {
2766 let mut client = connect_postgres_async(creds).await?;
2767
2768 if has_postgres_concurrent_index(statements) {
2769 for stmt in statements {
2771 let s = stmt.trim();
2772 if s.is_empty() {
2773 continue;
2774 }
2775 client
2776 .execute(s, &[])
2777 .await
2778 .map_err(|e| CliError::MigrationError(format!("Statement failed: {}\n{}", e, s)))?;
2779 }
2780 } else {
2781 let tx = client
2782 .transaction()
2783 .await
2784 .map_err(|e| CliError::MigrationError(e.to_string()))?;
2785
2786 for stmt in statements {
2787 let s = stmt.trim();
2788 if s.is_empty() {
2789 continue;
2790 }
2791 tx.execute(s, &[])
2792 .await
2793 .map_err(|e| CliError::MigrationError(format!("Statement failed: {}\n{}", e, s)))?;
2794 }
2795
2796 tx.commit()
2797 .await
2798 .map_err(|e| CliError::MigrationError(e.to_string()))?;
2799 }
2800
2801 Ok(())
2802}
2803
2804#[cfg(feature = "tokio-postgres")]
2805#[allow(dead_code)] fn run_postgres_async_migrations(
2807 set: &Migrations,
2808 creds: &PostgresCreds,
2809 repair: bool,
2810) -> Result<MigrationResult, CliError> {
2811 let rt = tokio::runtime::Builder::new_current_thread()
2812 .enable_all()
2813 .build()
2814 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
2815
2816 rt.block_on(run_postgres_async_inner(set, creds, repair))
2817}
2818
2819#[cfg(feature = "tokio-postgres")]
2820#[allow(dead_code)] fn inspect_postgres_async_migrations(
2822 set: &Migrations,
2823 creds: &PostgresCreds,
2824) -> Result<MigrationPlan, CliError> {
2825 let rt = tokio::runtime::Builder::new_current_thread()
2826 .enable_all()
2827 .build()
2828 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
2829
2830 rt.block_on(inspect_postgres_async_inner(set, creds))
2831}
2832
2833#[cfg(feature = "tokio-postgres")]
2834#[allow(dead_code)]
2835async fn inspect_postgres_async_inner(
2836 set: &Migrations,
2837 creds: &PostgresCreds,
2838) -> Result<MigrationPlan, CliError> {
2839 let client = connect_postgres_async(creds).await?;
2840
2841 let applied = match read_postgres_tracking_state_async(&client, set).await? {
2843 TrackingState::Missing => Vec::new(),
2844 TrackingState::Legacy(rows) => legacy_applied_records(set, &rows)?,
2845 TrackingState::Current => query_applied_records_postgres_async(&client, set).await?,
2846 };
2847 build_migration_plan(set, &applied)
2848}
2849
2850#[cfg(feature = "tokio-postgres")]
2851async fn query_applied_records_postgres_async(
2852 client: &tokio_postgres::Client,
2853 set: &Migrations,
2854) -> Result<Vec<AppliedMigrationRecord>, CliError> {
2855 let rows = client
2856 .query(&set.applied_records_sql(), &[])
2857 .await
2858 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2859
2860 let mut applied = Vec::new();
2861 for row in rows {
2862 let hash = row
2863 .try_get::<_, Option<String>>(0)
2864 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2865 let name = row
2866 .try_get::<_, Option<String>>(1)
2867 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2868 let dirty = row
2869 .try_get::<_, bool>(2)
2870 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2871 if let (Some(hash), Some(name)) = (hash, name) {
2872 applied.push(AppliedMigrationRecord { hash, name, dirty });
2873 }
2874 }
2875
2876 Ok(applied)
2877}
2878
2879#[cfg(feature = "tokio-postgres")]
2880#[allow(dead_code)]
2881async fn run_postgres_async_inner(
2882 set: &Migrations,
2883 creds: &PostgresCreds,
2884 repair: bool,
2885) -> Result<MigrationResult, CliError> {
2886 let mut client = connect_postgres_async(creds).await?;
2887
2888 let lock_key = set.postgres_advisory_lock_key();
2891 client
2892 .query_one("SELECT pg_advisory_lock($1)", &[&lock_key])
2893 .await
2894 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2895 let result = run_postgres_async_migrations_locked(&mut client, set, repair).await;
2896 let unlock = client
2897 .query_one("SELECT pg_advisory_unlock($1)", &[&lock_key])
2898 .await
2899 .map_err(|error| CliError::MigrationError(error.to_string()));
2900 match (result, unlock) {
2901 (Ok(result), Ok(_)) => Ok(result),
2902 (Err(error), _) => Err(error),
2903 (Ok(_), Err(error)) => Err(error),
2904 }
2905}
2906
2907#[cfg(feature = "tokio-postgres")]
2908async fn run_postgres_async_migrations_locked(
2909 client: &mut tokio_postgres::Client,
2910 set: &Migrations,
2911 repair: bool,
2912) -> Result<MigrationResult, CliError> {
2913 if let Some(schema_sql) = set.create_schema_sql() {
2914 client
2915 .execute(&schema_sql, &[])
2916 .await
2917 .map_err(|e| CliError::MigrationError(postgres_error_text(&e)))?;
2918 }
2919 ensure_postgres_tracking_table_async(client, set).await?;
2920 let repaired = repair_dirty_migrations_postgres_async(client, set, repair).await?;
2921 let rows = client
2922 .query(&set.applied_names_sql(), &[])
2923 .await
2924 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2925 let applied_names = rows
2926 .iter()
2927 .map(|row| row.try_get(0))
2928 .collect::<Result<Vec<String>, _>>()
2929 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2930 let pending: Vec<_> = set.pending(&applied_names).collect();
2931 if pending.is_empty() {
2932 return Ok(MigrationResult {
2933 applied_count: 0,
2934 applied_migrations: vec![],
2935 repaired_migrations: repaired,
2936 });
2937 }
2938
2939 let mut applied = Vec::new();
2940 if set.has_postgres_concurrent_index() {
2941 for migration in &pending {
2947 client
2948 .execute(&set.record_migration_started_sql(migration), &[])
2949 .await
2950 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2951
2952 let mut executed = 0usize;
2953 for statement in migration.statements() {
2954 if statement.trim().is_empty() {
2955 continue;
2956 }
2957 if let Err(error) = client.batch_execute(statement).await {
2958 if executed == 0 {
2961 let _ = client
2962 .execute(&set.clear_migration_started_sql(migration), &[])
2963 .await;
2964 }
2965 return Err(migration_statement_error(migration, statement, &error));
2966 }
2967 executed += 1;
2968 }
2969
2970 client
2971 .execute(&set.record_migration_finished_sql(migration), &[])
2972 .await
2973 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2974 applied.push(migration.hash().to_string());
2975 }
2976 } else {
2977 let transaction = client
2978 .transaction()
2979 .await
2980 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2981 for migration in &pending {
2982 for statement in migration.statements() {
2983 if !statement.trim().is_empty() {
2984 transaction
2986 .batch_execute(statement)
2987 .await
2988 .map_err(|error| migration_statement_error(migration, statement, &error))?;
2989 }
2990 }
2991 transaction
2992 .execute(&set.record_migration_sql(migration), &[])
2993 .await
2994 .map_err(|error| CliError::MigrationError(error.to_string()))?;
2995 applied.push(migration.hash().to_string());
2996 }
2997 transaction
2998 .commit()
2999 .await
3000 .map_err(|error| CliError::MigrationError(error.to_string()))?;
3001 }
3002
3003 Ok(MigrationResult {
3004 applied_count: applied.len(),
3005 applied_migrations: applied,
3006 repaired_migrations: repaired,
3007 })
3008}
3009
3010#[cfg(feature = "libsql")]
3015fn query_libsql_local_counts(path: &str, queries: &[String]) -> Result<Vec<i64>, CliError> {
3016 let rt = tokio::runtime::Builder::new_current_thread()
3017 .enable_all()
3018 .build()
3019 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
3020
3021 rt.block_on(async {
3022 let db = libsql::Builder::new_local(path)
3023 .build()
3024 .await
3025 .map_err(|e| {
3026 CliError::ConnectionError(format!("Failed to open LibSQL database '{path}': {e}"))
3027 })?;
3028 let conn = db
3029 .connect()
3030 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
3031 query_libsql_counts_on_connection(&conn, queries).await
3032 })
3033}
3034
3035#[cfg(any(feature = "libsql", feature = "turso"))]
3037async fn query_libsql_counts_on_connection(
3038 conn: &libsql::Connection,
3039 queries: &[String],
3040) -> Result<Vec<i64>, CliError> {
3041 let mut counts = Vec::with_capacity(queries.len());
3042 for query in queries {
3043 let mut rows = conn
3044 .query(query, ())
3045 .await
3046 .map_err(|error| count_query_error(query, error))?;
3047 let row = rows
3048 .next()
3049 .await
3050 .map_err(|error| count_query_error(query, error))?
3051 .ok_or_else(|| count_query_error(query, "no row returned"))?;
3052 counts.push(
3053 row.get::<i64>(0)
3054 .map_err(|error| count_query_error(query, error))?,
3055 );
3056 }
3057 Ok(counts)
3058}
3059
3060#[cfg(feature = "libsql")]
3061fn execute_libsql_local_statements(path: &str, statements: &[String]) -> Result<(), CliError> {
3062 let rt = tokio::runtime::Builder::new_current_thread()
3063 .enable_all()
3064 .build()
3065 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
3066
3067 rt.block_on(execute_libsql_local_inner(path, statements))
3068}
3069
3070#[cfg(feature = "libsql")]
3071async fn execute_libsql_local_inner(path: &str, statements: &[String]) -> Result<(), CliError> {
3072 let db = libsql::Builder::new_local(path)
3073 .build()
3074 .await
3075 .map_err(|e| {
3076 CliError::ConnectionError(format!("Failed to open LibSQL database '{path}': {e}"))
3077 })?;
3078
3079 let conn = db
3080 .connect()
3081 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
3082
3083 execute_libsql_sqlite_statements_on_connection(&conn, statements).await
3084}
3085
3086#[cfg(any(feature = "libsql", feature = "turso"))]
3087async fn execute_libsql_sqlite_statements_on_connection(
3088 conn: &libsql::Connection,
3089 statements: &[String],
3090) -> Result<(), CliError> {
3091 let operation =
3092 drizzle_migrations::Migration::with_hash("libsql_push", "", 0, statements.to_vec());
3093 let execution = operation
3094 .sqlite_execution()
3095 .map_err(sqlite_execution_cli_error)?;
3096 let foreign_keys_were_enabled = sqlite_foreign_keys_enabled_libsql_cli(conn).await?;
3097 let suspend_foreign_keys = execution.suspends_foreign_keys() && foreign_keys_were_enabled;
3098 if suspend_foreign_keys
3099 && let Err(error) = set_sqlite_foreign_keys_libsql_cli(conn, false).await
3100 {
3101 let restore = set_sqlite_foreign_keys_libsql_cli(conn, true).await;
3102 return finish_sqlite_foreign_key_scope(Err(error), restore);
3103 }
3104
3105 let result = async {
3106 let tx = conn
3107 .transaction_with_behavior(libsql::TransactionBehavior::Immediate)
3108 .await
3109 .map_err(|error| CliError::MigrationError(error.to_string()))?;
3110 let transaction_result = async {
3111 for statement in execution
3112 .statements()
3113 .filter(|statement| !statement.trim().is_empty())
3114 {
3115 tx.execute(statement, ()).await.map_err(|error| {
3116 CliError::MigrationError(format!("Statement failed: {error}\n{statement}"))
3117 })?;
3118 }
3119 if execution.suspends_foreign_keys() {
3120 verify_sqlite_foreign_keys_libsql_cli(&tx).await?;
3121 }
3122 Ok::<_, CliError>(())
3123 }
3124 .await;
3125 match transaction_result {
3126 Ok(()) => tx
3127 .commit()
3128 .await
3129 .map_err(|error| CliError::MigrationError(error.to_string())),
3130 Err(error) => {
3131 tx.rollback().await.ok();
3132 Err(error)
3133 }
3134 }
3135 }
3136 .await;
3137 let restore = if suspend_foreign_keys {
3138 set_sqlite_foreign_keys_libsql_cli(conn, true).await
3139 } else {
3140 Ok(())
3141 };
3142 finish_sqlite_foreign_key_scope(result, restore)
3143}
3144
3145#[cfg(feature = "libsql")]
3146fn run_libsql_local_migrations(
3147 set: &Migrations,
3148 path: &str,
3149 repair: bool,
3150) -> Result<MigrationResult, CliError> {
3151 let rt = tokio::runtime::Builder::new_current_thread()
3152 .enable_all()
3153 .build()
3154 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
3155
3156 rt.block_on(run_libsql_local_inner(set, path, repair))
3157}
3158
3159#[cfg(feature = "libsql")]
3160fn inspect_libsql_local_migrations(
3161 set: &Migrations,
3162 path: &str,
3163) -> Result<MigrationPlan, CliError> {
3164 let rt = tokio::runtime::Builder::new_current_thread()
3165 .enable_all()
3166 .build()
3167 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
3168
3169 rt.block_on(inspect_libsql_local_inner(set, path))
3170}
3171
3172#[cfg(feature = "libsql")]
3173async fn inspect_libsql_local_inner(
3174 set: &Migrations,
3175 path: &str,
3176) -> Result<MigrationPlan, CliError> {
3177 if !sqlite_database_exists(path) {
3178 return build_migration_plan(set, &[]);
3179 }
3180 let db = libsql::Builder::new_local(path)
3181 .build()
3182 .await
3183 .map_err(|e| {
3184 CliError::ConnectionError(format!("Failed to open LibSQL database '{path}': {e}"))
3185 })?;
3186
3187 let conn = db
3188 .connect()
3189 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
3190
3191 let applied = match read_sqlite_tracking_state_libsql(&conn, set).await? {
3192 TrackingState::Missing => Vec::new(),
3193 TrackingState::Legacy(rows) => legacy_applied_records(set, &rows)?,
3194 TrackingState::Current => query_applied_records_libsql(&conn, set).await?,
3195 };
3196 build_migration_plan(set, &applied)
3197}
3198
3199#[cfg(feature = "libsql")]
3200async fn run_libsql_local_inner(
3201 set: &Migrations,
3202 path: &str,
3203 repair: bool,
3204) -> Result<MigrationResult, CliError> {
3205 let db = libsql::Builder::new_local(path)
3206 .build()
3207 .await
3208 .map_err(|e| {
3209 CliError::ConnectionError(format!("Failed to open LibSQL database '{path}': {e}"))
3210 })?;
3211
3212 let conn = db
3213 .connect()
3214 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
3215
3216 run_libsql_sqlite_migrations_on_connection(set, &conn, repair).await
3217}
3218
3219#[cfg(any(feature = "libsql", feature = "turso"))]
3220async fn query_applied_names_libsql(
3221 conn: &libsql::Connection,
3222 set: &Migrations,
3223) -> Result<Vec<String>, CliError> {
3224 let mut rows = conn
3225 .query(&set.applied_names_sql(), ())
3226 .await
3227 .map_err(|error| CliError::MigrationError(error.to_string()))?;
3228
3229 let mut names = Vec::new();
3230 while let Some(row) = rows
3231 .next()
3232 .await
3233 .map_err(|error| CliError::MigrationError(error.to_string()))?
3234 {
3235 names.push(
3236 row.get::<String>(0)
3237 .map_err(|error| CliError::MigrationError(error.to_string()))?,
3238 );
3239 }
3240
3241 Ok(names)
3242}
3243
3244#[cfg(any(feature = "libsql", feature = "turso"))]
3245async fn run_libsql_sqlite_migrations_on_connection(
3246 set: &Migrations,
3247 conn: &libsql::Connection,
3248 repair: bool,
3249) -> Result<MigrationResult, CliError> {
3250 ensure_sqlite_tracking_table_libsql(conn, set).await?;
3251 let applied_before_transaction = query_applied_names_libsql(conn, set).await?;
3252 let suspends_foreign_keys = set
3253 .pending(&applied_before_transaction)
3254 .map(|migration| migration.sqlite_execution())
3255 .collect::<Result<Vec<_>, _>>()
3256 .map_err(sqlite_execution_cli_error)?
3257 .iter()
3258 .any(|execution| execution.suspends_foreign_keys());
3259 let foreign_keys_were_enabled = sqlite_foreign_keys_enabled_libsql_cli(conn).await?;
3260 let suspend_foreign_keys = suspends_foreign_keys && foreign_keys_were_enabled;
3261 if suspend_foreign_keys
3262 && let Err(error) = set_sqlite_foreign_keys_libsql_cli(conn, false).await
3263 {
3264 let restore = set_sqlite_foreign_keys_libsql_cli(conn, true).await;
3265 return finish_sqlite_foreign_key_scope(Err(error), restore);
3266 }
3267
3268 let result = async {
3269 let repaired = repair_dirty_migrations_libsql(conn, set, repair).await?;
3270 let tx = conn
3271 .transaction_with_behavior(libsql::TransactionBehavior::Immediate)
3272 .await
3273 .map_err(|error| CliError::MigrationError(error.to_string()))?;
3274 let transaction_result = async {
3275 let applied_names = query_applied_names_libsql(&tx, set).await?;
3276 let pending = set
3277 .pending(&applied_names)
3278 .map(|migration| {
3279 migration
3280 .sqlite_execution()
3281 .map(|execution| (migration, execution))
3282 })
3283 .collect::<Result<Vec<_>, _>>()
3284 .map_err(sqlite_execution_cli_error)?;
3285
3286 let mut applied = Vec::with_capacity(pending.len());
3287 for (migration, execution) in pending {
3288 for statement in execution
3289 .statements()
3290 .filter(|statement| !statement.trim().is_empty())
3291 {
3292 tx.execute(statement, ()).await.map_err(|error| {
3293 CliError::MigrationError(format!(
3294 "Migration '{}' failed: {error}",
3295 migration.hash()
3296 ))
3297 })?;
3298 }
3299 tx.execute(&set.record_migration_sql(migration), ())
3300 .await
3301 .map_err(|error| CliError::MigrationError(error.to_string()))?;
3302 applied.push(migration.hash().to_string());
3303 }
3304
3305 Ok::<_, CliError>(MigrationResult {
3306 applied_count: applied.len(),
3307 applied_migrations: applied,
3308 repaired_migrations: repaired,
3309 })
3310 }
3311 .await;
3312
3313 match transaction_result {
3314 Ok(outcome) => {
3315 if suspends_foreign_keys
3316 && let Err(error) = verify_sqlite_foreign_keys_libsql_cli(&tx).await
3317 {
3318 tx.rollback().await.ok();
3319 return Err(error);
3320 }
3321 tx.commit()
3322 .await
3323 .map_err(|error| CliError::MigrationError(error.to_string()))?;
3324 Ok(outcome)
3325 }
3326 Err(error) => {
3327 tx.rollback().await.ok();
3328 Err(error)
3329 }
3330 }
3331 }
3332 .await;
3333 let restore = if suspend_foreign_keys {
3334 set_sqlite_foreign_keys_libsql_cli(conn, true).await
3335 } else {
3336 Ok(())
3337 };
3338 finish_sqlite_foreign_key_scope(result, restore)
3339}
3340
3341#[cfg(any(feature = "libsql", feature = "turso"))]
3342async fn sqlite_foreign_keys_enabled_libsql_cli(
3343 conn: &libsql::Connection,
3344) -> Result<bool, CliError> {
3345 let mut rows = conn
3346 .query("PRAGMA foreign_keys", ())
3347 .await
3348 .map_err(|error| CliError::MigrationError(error.to_string()))?;
3349 let row = rows
3350 .next()
3351 .await
3352 .map_err(|error| CliError::MigrationError(error.to_string()))?
3353 .ok_or_else(|| CliError::MigrationError("PRAGMA foreign_keys returned no row".into()))?;
3354 row.get::<i64>(0)
3355 .map(|enabled| enabled != 0)
3356 .map_err(|error| CliError::MigrationError(error.to_string()))
3357}
3358
3359#[cfg(any(feature = "libsql", feature = "turso"))]
3360async fn set_sqlite_foreign_keys_libsql_cli(
3361 conn: &libsql::Connection,
3362 enabled: bool,
3363) -> Result<(), CliError> {
3364 conn.execute(
3365 if enabled {
3366 "PRAGMA foreign_keys=ON"
3367 } else {
3368 "PRAGMA foreign_keys=OFF"
3369 },
3370 (),
3371 )
3372 .await
3373 .map_err(|error| CliError::MigrationError(error.to_string()))?;
3374 if sqlite_foreign_keys_enabled_libsql_cli(conn).await? != enabled {
3375 return Err(CliError::MigrationError(format!(
3376 "SQLite refused to set foreign_keys={}",
3377 if enabled { "ON" } else { "OFF" }
3378 )));
3379 }
3380 Ok(())
3381}
3382
3383#[cfg(any(feature = "libsql", feature = "turso"))]
3384async fn verify_sqlite_foreign_keys_libsql_cli(conn: &libsql::Connection) -> Result<(), CliError> {
3385 let mut rows = conn
3386 .query("PRAGMA foreign_key_check", ())
3387 .await
3388 .map_err(|error| CliError::MigrationError(error.to_string()))?;
3389 if rows
3390 .next()
3391 .await
3392 .map_err(|error| CliError::MigrationError(error.to_string()))?
3393 .is_some()
3394 {
3395 return Err(CliError::MigrationError(
3396 "SQLite foreign_key_check failed after migration rebuild".into(),
3397 ));
3398 }
3399 Ok(())
3400}
3401
3402#[cfg(feature = "libsql")]
3403async fn query_applied_records_libsql(
3404 conn: &libsql::Connection,
3405 set: &Migrations,
3406) -> Result<Vec<AppliedMigrationRecord>, CliError> {
3407 let mut rows = conn
3408 .query(&set.applied_records_sql(), ())
3409 .await
3410 .map_err(|error| CliError::MigrationError(error.to_string()))?;
3411
3412 let mut applied = Vec::new();
3413 while let Some(row) = rows
3414 .next()
3415 .await
3416 .map_err(|error| CliError::MigrationError(error.to_string()))?
3417 {
3418 let hash = row
3419 .get::<String>(0)
3420 .map_err(|error| CliError::MigrationError(error.to_string()))?;
3421 let name = row
3422 .get::<String>(1)
3423 .map_err(|error| CliError::MigrationError(error.to_string()))?;
3424 let dirty = row
3425 .get::<i64>(2)
3426 .map_err(|error| CliError::MigrationError(error.to_string()))?
3427 != 0;
3428 applied.push(AppliedMigrationRecord { hash, name, dirty });
3429 }
3430
3431 Ok(applied)
3432}
3433
3434#[cfg(feature = "turso")]
3439fn query_turso_counts(
3440 url: &str,
3441 auth_token: Option<&str>,
3442 queries: &[String],
3443) -> Result<Vec<i64>, CliError> {
3444 let rt = tokio::runtime::Builder::new_current_thread()
3445 .enable_all()
3446 .build()
3447 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
3448
3449 rt.block_on(async {
3450 let builder =
3451 libsql::Builder::new_remote(url.to_string(), auth_token.unwrap_or("").to_string());
3452 let db = builder.build().await.map_err(|e| {
3453 CliError::ConnectionError(format!("Failed to connect to Turso '{url}': {e}"))
3454 })?;
3455 let conn = db
3456 .connect()
3457 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
3458 query_libsql_counts_on_connection(&conn, queries).await
3459 })
3460}
3461
3462#[cfg(feature = "turso")]
3463fn execute_turso_statements(
3464 url: &str,
3465 auth_token: Option<&str>,
3466 statements: &[String],
3467) -> Result<(), CliError> {
3468 let rt = tokio::runtime::Builder::new_current_thread()
3469 .enable_all()
3470 .build()
3471 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
3472
3473 rt.block_on(execute_turso_inner(url, auth_token, statements))
3474}
3475
3476#[cfg(feature = "turso")]
3477async fn execute_turso_inner(
3478 url: &str,
3479 auth_token: Option<&str>,
3480 statements: &[String],
3481) -> Result<(), CliError> {
3482 let builder =
3483 libsql::Builder::new_remote(url.to_string(), auth_token.unwrap_or("").to_string());
3484
3485 let db = builder.build().await.map_err(|e| {
3486 CliError::ConnectionError(format!("Failed to connect to Turso '{url}': {e}"))
3487 })?;
3488
3489 let conn = db
3490 .connect()
3491 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
3492
3493 execute_libsql_sqlite_statements_on_connection(&conn, statements).await
3494}
3495
3496#[cfg(feature = "turso")]
3497fn run_turso_migrations(
3498 set: &Migrations,
3499 url: &str,
3500 auth_token: Option<&str>,
3501 repair: bool,
3502) -> Result<MigrationResult, CliError> {
3503 let rt = tokio::runtime::Builder::new_current_thread()
3504 .enable_all()
3505 .build()
3506 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
3507
3508 rt.block_on(run_turso_inner(set, url, auth_token, repair))
3509}
3510
3511#[cfg(feature = "turso")]
3512fn inspect_turso_migrations(
3513 set: &Migrations,
3514 url: &str,
3515 auth_token: Option<&str>,
3516) -> Result<MigrationPlan, CliError> {
3517 let rt = tokio::runtime::Builder::new_current_thread()
3518 .enable_all()
3519 .build()
3520 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
3521
3522 rt.block_on(inspect_turso_inner(set, url, auth_token))
3523}
3524
3525#[cfg(feature = "turso")]
3526async fn inspect_turso_inner(
3527 set: &Migrations,
3528 url: &str,
3529 auth_token: Option<&str>,
3530) -> Result<MigrationPlan, CliError> {
3531 let builder =
3532 libsql::Builder::new_remote(url.to_string(), auth_token.unwrap_or("").to_string());
3533
3534 let db = builder.build().await.map_err(|e| {
3535 CliError::ConnectionError(format!("Failed to connect to Turso '{url}': {e}"))
3536 })?;
3537
3538 let conn = db
3539 .connect()
3540 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
3541
3542 let applied = match read_sqlite_tracking_state_libsql(&conn, set).await? {
3543 TrackingState::Missing => Vec::new(),
3544 TrackingState::Legacy(rows) => legacy_applied_records(set, &rows)?,
3545 TrackingState::Current => query_applied_records_turso(&conn, set).await?,
3546 };
3547 build_migration_plan(set, &applied)
3548}
3549
3550#[cfg(feature = "turso")]
3551async fn run_turso_inner(
3552 set: &Migrations,
3553 url: &str,
3554 auth_token: Option<&str>,
3555 repair: bool,
3556) -> Result<MigrationResult, CliError> {
3557 let builder =
3558 libsql::Builder::new_remote(url.to_string(), auth_token.unwrap_or("").to_string());
3559
3560 let db = builder.build().await.map_err(|e| {
3561 CliError::ConnectionError(format!("Failed to connect to Turso '{url}': {e}"))
3562 })?;
3563
3564 let conn = db
3565 .connect()
3566 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
3567
3568 run_libsql_sqlite_migrations_on_connection(set, &conn, repair).await
3569}
3570
3571#[cfg(feature = "turso")]
3572async fn query_applied_records_turso(
3573 conn: &libsql::Connection,
3574 set: &Migrations,
3575) -> Result<Vec<AppliedMigrationRecord>, CliError> {
3576 let mut rows = conn
3577 .query(&set.applied_records_sql(), ())
3578 .await
3579 .map_err(|error| CliError::MigrationError(error.to_string()))?;
3580
3581 let mut applied = Vec::new();
3582 while let Some(row) = rows
3583 .next()
3584 .await
3585 .map_err(|error| CliError::MigrationError(error.to_string()))?
3586 {
3587 let hash = row
3588 .get::<String>(0)
3589 .map_err(|error| CliError::MigrationError(error.to_string()))?;
3590 let name = row
3591 .get::<String>(1)
3592 .map_err(|error| CliError::MigrationError(error.to_string()))?;
3593 let dirty = row
3594 .get::<i64>(2)
3595 .map_err(|error| CliError::MigrationError(error.to_string()))?
3596 != 0;
3597 applied.push(AppliedMigrationRecord { hash, name, dirty });
3598 }
3599
3600 Ok(applied)
3601}
3602
3603#[derive(Debug)]
3609pub struct IntrospectResult {
3610 pub schema_code: String,
3612 pub table_count: usize,
3614 pub index_count: usize,
3616 pub view_count: usize,
3618 pub warnings: Vec<String>,
3620 pub mysql_catalog_defaults: Option<drizzle_migrations::mysql::MySQLCatalogDefaults>,
3623 pub snapshot: Snapshot,
3625 pub snapshot_path: std::path::PathBuf,
3627}
3628
3629#[allow(clippy::too_many_arguments)]
3645pub fn run_introspection(
3646 connection: &ResolvedConnection,
3647 out_dir: &Path,
3648 init_metadata: bool,
3649 breakpoints: bool,
3650 introspect_casing: Option<IntrospectCasing>,
3651 filters: &SnapshotFilters,
3652 migrations_table: &str,
3653 migrations_schema: &str,
3654) -> Result<IntrospectResult, CliError> {
3655 use drizzle_migrations::naming::generate_migration_tag;
3656
3657 let mut result = introspect_database(connection)?;
3659 exclude_tracking_table(&mut result.snapshot, connection.dialect, migrations_table)?;
3660 apply_snapshot_filters(&mut result.snapshot, connection.dialect, filters)?;
3661 if !filters.is_empty() || introspect_casing.is_some() {
3662 regenerate_schema_from_snapshot(&mut result, connection.dialect, introspect_casing);
3663 }
3664
3665 let schema_path = out_dir.join("schema.rs");
3667 if let Some(parent) = schema_path.parent() {
3668 std::fs::create_dir_all(parent).map_err(|e| {
3669 CliError::Other(format!(
3670 "Failed to create output directory '{}': {}",
3671 parent.display(),
3672 e
3673 ))
3674 })?;
3675 }
3676 std::fs::write(&schema_path, &result.schema_code).map_err(|e| {
3677 CliError::Other(format!(
3678 "Failed to write schema file '{}': {}",
3679 schema_path.display(),
3680 e
3681 ))
3682 })?;
3683
3684 let journal_path = out_dir.join("meta").join("_journal.json");
3685 if journal_path.exists() {
3686 return Err(CliError::Other(
3687 "Detected old drizzle-kit migration folders. Upgrade them before writing new migrations."
3688 .to_string(),
3689 ));
3690 }
3691
3692 let tag = generate_migration_tag(None);
3694
3695 let migration_dir = out_dir.join(&tag);
3697 std::fs::create_dir_all(&migration_dir).map_err(|e| {
3698 CliError::Other(format!(
3699 "Failed to create migration directory '{}': {}",
3700 migration_dir.display(),
3701 e
3702 ))
3703 })?;
3704
3705 let snapshot_path = migration_dir.join("snapshot.json");
3707 result.snapshot.save(&snapshot_path).map_err(|e| {
3708 CliError::Other(format!(
3709 "Failed to write snapshot file '{}': {}",
3710 snapshot_path.display(),
3711 e
3712 ))
3713 })?;
3714
3715 let base_dialect = connection.dialect.to_base();
3717 let empty_snapshot = Snapshot::empty(base_dialect);
3718 let sql_statements =
3719 generate_introspect_migration(&empty_snapshot, &result.snapshot, breakpoints)?;
3720
3721 let migration_sql_path = migration_dir.join("migration.sql");
3725 let mut sql_content = format_migration_sql(&sql_statements, breakpoints);
3726 if !init_metadata {
3727 sql_content = format!(
3728 "-- Current sql file was generated after introspecting the database\n\
3729 -- If you want to run this migration please uncomment this code before executing migrations\n\
3730 /*\n{sql_content}\n*/"
3731 );
3732 }
3733 std::fs::write(&migration_sql_path, &sql_content).map_err(|e| {
3734 CliError::Other(format!(
3735 "Failed to write migration file '{}': {}",
3736 migration_sql_path.display(),
3737 e
3738 ))
3739 })?;
3740
3741 result.snapshot_path = snapshot_path;
3743
3744 if init_metadata {
3745 apply_init_metadata(connection, out_dir, migrations_table, migrations_schema)?;
3746 }
3747
3748 Ok(result)
3749}
3750
3751#[allow(unused_variables)] fn apply_init_metadata(
3757 connection: &ResolvedConnection,
3758 out_dir: &Path,
3759 migrations_table: &str,
3760 migrations_schema: &str,
3761) -> Result<(), CliError> {
3762 let set = load_migration_set(
3763 connection.dialect,
3764 out_dir,
3765 migrations_table,
3766 migrations_schema,
3767 )?;
3768
3769 match &connection.credentials {
3770 #[cfg(feature = "rusqlite")]
3771 Credentials::Sqlite { path } if connection.driver == Driver::Rusqlite => {
3772 init_sqlite_metadata(path, &set)
3773 }
3774
3775 #[cfg(feature = "rusqlite")]
3776 Credentials::Sqlite { .. } => Err(connection_driver_mismatch(connection)),
3777
3778 #[cfg(not(feature = "rusqlite"))]
3779 Credentials::Sqlite { .. } => Err(CliError::MissingDriver {
3780 dialect: "SQLite",
3781 feature: "rusqlite",
3782 }),
3783
3784 #[cfg(any(feature = "libsql", feature = "turso"))]
3785 Credentials::Turso { url, auth_token } => {
3786 if connection.driver == Driver::Libsql {
3787 #[cfg(feature = "libsql")]
3788 {
3789 init_libsql_local_metadata(url, &set)
3790 }
3791 #[cfg(not(feature = "libsql"))]
3792 {
3793 Err(CliError::MissingDriver {
3794 dialect: "LibSQL (local)",
3795 feature: "libsql",
3796 })
3797 }
3798 } else if connection.driver == Driver::Turso {
3799 #[cfg(feature = "turso")]
3800 {
3801 init_turso_metadata(url, auth_token.as_deref(), &set)
3802 }
3803 #[cfg(not(feature = "turso"))]
3804 {
3805 let _ = auth_token;
3806 Err(CliError::MissingDriver {
3807 dialect: "Turso (remote)",
3808 feature: "turso",
3809 })
3810 }
3811 } else {
3812 Err(connection_driver_mismatch(connection))
3813 }
3814 }
3815
3816 #[cfg(all(not(feature = "turso"), not(feature = "libsql")))]
3817 Credentials::Turso { .. } => Err(CliError::MissingDriver {
3818 dialect: "Turso",
3819 feature: "turso or libsql",
3820 }),
3821
3822 Credentials::Postgres(creds) => {
3823 let _ = creds;
3824 match connection.driver {
3825 Driver::PostgresSync => {
3826 #[cfg(feature = "postgres-sync")]
3827 {
3828 init_postgres_sync_metadata(creds, &set)
3829 }
3830 #[cfg(not(feature = "postgres-sync"))]
3831 {
3832 Err(CliError::MissingDriver {
3833 dialect: "PostgreSQL",
3834 feature: "postgres-sync",
3835 })
3836 }
3837 }
3838 Driver::TokioPostgres => {
3839 #[cfg(feature = "tokio-postgres")]
3840 {
3841 init_postgres_async_metadata(creds, &set)
3842 }
3843 #[cfg(not(feature = "tokio-postgres"))]
3844 {
3845 Err(CliError::MissingDriver {
3846 dialect: "PostgreSQL",
3847 feature: "tokio-postgres",
3848 })
3849 }
3850 }
3851 _ => Err(connection_driver_mismatch(connection)),
3852 }
3853 }
3854
3855 Credentials::MySQL(creds) => mysql::init_metadata(connection.driver, creds, &set),
3856
3857 #[cfg(feature = "d1-http")]
3858 Credentials::D1 {
3859 account_id,
3860 database_id,
3861 token,
3862 } if connection.driver == Driver::D1Http => {
3863 d1_http::init_metadata(&set, account_id, database_id, token)
3864 }
3865
3866 #[cfg(feature = "d1-http")]
3867 Credentials::D1 { .. } => Err(connection_driver_mismatch(connection)),
3868
3869 #[cfg(not(feature = "d1-http"))]
3870 Credentials::D1 { .. } => Err(CliError::MissingDriver {
3871 dialect: "Cloudflare D1 (HTTP)",
3872 feature: "d1-http",
3873 }),
3874
3875 Credentials::AwsDataApi { .. } => Err(CliError::UnsupportedForDriver {
3876 operation: "Migration metadata init against AWS Data API",
3877 driver: "aws-data-api",
3878 hint: "AWS RDS Data API schema ops are not yet wired into this CLI. Seed the \
3879 migrations table manually via `aws rds-data execute-statement` or \
3880 tokio-postgres.",
3881 }),
3882 }
3883}
3884
3885#[cfg(any(
3886 feature = "rusqlite",
3887 feature = "libsql",
3888 feature = "turso",
3889 feature = "postgres-sync",
3890 feature = "tokio-postgres",
3891 feature = "mysql-sync",
3892 feature = "mysql-async",
3893 feature = "d1-http",
3894))]
3895pub(crate) fn validate_init_metadata(
3896 applied_names: &[String],
3897 set: &Migrations,
3898) -> Result<(), CliError> {
3899 if !applied_names.is_empty() {
3900 return Err(CliError::Other(
3901 "--init can't be used when database already has migrations set".into(),
3902 ));
3903 }
3904
3905 if set.all().len() > 1 {
3906 return Err(CliError::Other(
3907 "--init can't be used with existing migrations".into(),
3908 ));
3909 }
3910
3911 Ok(())
3912}
3913
3914#[cfg(feature = "rusqlite")]
3919fn init_sqlite_metadata(path: &str, set: &Migrations) -> Result<(), CliError> {
3920 let conn = rusqlite::Connection::open(path).map_err(|e| {
3921 CliError::ConnectionError(format!("Failed to open SQLite database '{path}': {e}"))
3922 })?;
3923
3924 ensure_sqlite_tracking_table(&conn, set)?;
3925
3926 let applied_names = query_applied_names_sqlite(&conn, set)?;
3927 validate_init_metadata(&applied_names, set)?;
3928
3929 let Some(first) = set.all().first() else {
3930 return Ok(());
3931 };
3932
3933 conn.execute(&set.record_migration_sql(first), [])
3934 .map_err(|e| CliError::MigrationError(e.to_string()))?;
3935
3936 Ok(())
3937}
3938
3939#[cfg(feature = "libsql")]
3940fn init_libsql_local_metadata(path: &str, set: &Migrations) -> Result<(), CliError> {
3941 let rt = tokio::runtime::Builder::new_current_thread()
3942 .enable_all()
3943 .build()
3944 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
3945
3946 rt.block_on(init_libsql_local_metadata_inner(path, set))
3947}
3948
3949#[cfg(feature = "libsql")]
3950async fn init_libsql_local_metadata_inner(path: &str, set: &Migrations) -> Result<(), CliError> {
3951 let db = libsql::Builder::new_local(path)
3952 .build()
3953 .await
3954 .map_err(|e| {
3955 CliError::ConnectionError(format!("Failed to open LibSQL database '{path}': {e}"))
3956 })?;
3957
3958 let conn = db
3959 .connect()
3960 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
3961
3962 ensure_sqlite_tracking_table_libsql(&conn, set).await?;
3963
3964 let applied_names = query_applied_names_libsql(&conn, set).await?;
3965 validate_init_metadata(&applied_names, set)?;
3966
3967 let Some(first) = set.all().first() else {
3968 return Ok(());
3969 };
3970
3971 conn.execute(&set.record_migration_sql(first), ())
3972 .await
3973 .map_err(|e| CliError::MigrationError(e.to_string()))?;
3974
3975 Ok(())
3976}
3977
3978#[cfg(feature = "turso")]
3979fn init_turso_metadata(
3980 url: &str,
3981 auth_token: Option<&str>,
3982 set: &Migrations,
3983) -> Result<(), CliError> {
3984 let rt = tokio::runtime::Builder::new_current_thread()
3985 .enable_all()
3986 .build()
3987 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
3988
3989 rt.block_on(init_turso_metadata_inner(url, auth_token, set))
3990}
3991
3992#[cfg(feature = "turso")]
3993async fn init_turso_metadata_inner(
3994 url: &str,
3995 auth_token: Option<&str>,
3996 set: &Migrations,
3997) -> Result<(), CliError> {
3998 let builder =
3999 libsql::Builder::new_remote(url.to_string(), auth_token.unwrap_or("").to_string());
4000
4001 let db = builder.build().await.map_err(|e| {
4002 CliError::ConnectionError(format!("Failed to connect to Turso '{url}': {e}"))
4003 })?;
4004
4005 let conn = db
4006 .connect()
4007 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
4008
4009 ensure_sqlite_tracking_table_libsql(&conn, set).await?;
4010
4011 let applied_names = query_applied_names_libsql(&conn, set).await?;
4012 validate_init_metadata(&applied_names, set)?;
4013
4014 let Some(first) = set.all().first() else {
4015 return Ok(());
4016 };
4017
4018 conn.execute(&set.record_migration_sql(first), ())
4019 .await
4020 .map_err(|e| CliError::MigrationError(e.to_string()))?;
4021
4022 Ok(())
4023}
4024
4025#[cfg(feature = "postgres-sync")]
4026fn init_postgres_sync_metadata(creds: &PostgresCreds, set: &Migrations) -> Result<(), CliError> {
4027 let mut client = connect_postgres_sync(creds)?;
4028
4029 if let Some(schema_sql) = set.create_schema_sql() {
4030 client
4031 .execute(&schema_sql, &[])
4032 .map_err(|e| CliError::MigrationError(e.to_string()))?;
4033 }
4034
4035 ensure_postgres_tracking_table_sync(&mut client, set)?;
4036
4037 let rows = client
4038 .query(&set.applied_names_sql(), &[])
4039 .map_err(|error| CliError::MigrationError(error.to_string()))?;
4040 let applied_names = rows
4041 .iter()
4042 .map(|row| row.try_get(0))
4043 .collect::<Result<Vec<String>, _>>()
4044 .map_err(|error| CliError::MigrationError(error.to_string()))?;
4045
4046 validate_init_metadata(&applied_names, set)?;
4047
4048 let Some(first) = set.all().first() else {
4049 return Ok(());
4050 };
4051
4052 client
4053 .execute(&set.record_migration_sql(first), &[])
4054 .map_err(|e| CliError::MigrationError(e.to_string()))?;
4055
4056 Ok(())
4057}
4058
4059#[cfg(feature = "tokio-postgres")]
4060fn init_postgres_async_metadata(creds: &PostgresCreds, set: &Migrations) -> Result<(), CliError> {
4061 let rt = tokio::runtime::Builder::new_current_thread()
4062 .enable_all()
4063 .build()
4064 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {}", e)))?;
4065
4066 rt.block_on(init_postgres_async_inner(creds, set))
4067}
4068
4069#[cfg(feature = "tokio-postgres")]
4070async fn init_postgres_async_inner(
4071 creds: &PostgresCreds,
4072 set: &Migrations,
4073) -> Result<(), CliError> {
4074 let client = connect_postgres_async(creds).await?;
4075
4076 if let Some(schema_sql) = set.create_schema_sql() {
4077 client
4078 .execute(&schema_sql, &[])
4079 .await
4080 .map_err(|e| CliError::MigrationError(e.to_string()))?;
4081 }
4082
4083 ensure_postgres_tracking_table_async(&client, set).await?;
4084
4085 let rows = client
4086 .query(&set.applied_names_sql(), &[])
4087 .await
4088 .map_err(|error| CliError::MigrationError(error.to_string()))?;
4089 let applied_names = rows
4090 .iter()
4091 .map(|row| row.try_get(0))
4092 .collect::<Result<Vec<String>, _>>()
4093 .map_err(|error| CliError::MigrationError(error.to_string()))?;
4094
4095 validate_init_metadata(&applied_names, set)?;
4096
4097 let Some(first) = set.all().first() else {
4098 return Ok(());
4099 };
4100
4101 client
4102 .execute(&set.record_migration_sql(first), &[])
4103 .await
4104 .map_err(|e| CliError::MigrationError(e.to_string()))?;
4105
4106 Ok(())
4107}
4108
4109fn generate_introspect_migration(
4111 prev: &Snapshot,
4112 current: &Snapshot,
4113 _breakpoints: bool,
4114) -> Result<Vec<String>, CliError> {
4115 drizzle_migrations::diff(prev, current)
4116 .map(|plan| plan.statements)
4117 .map_err(|error| CliError::MigrationError(error.to_string()))
4118}
4119
4120fn format_migration_sql(sql_statements: &[String], breakpoints: bool) -> String {
4121 if sql_statements.is_empty() {
4122 "-- No tables to create (empty database)\n".to_string()
4123 } else if breakpoints {
4124 sql_statements.join("\n--> statement-breakpoint\n")
4125 } else {
4126 sql_statements.join("\n\n")
4127 }
4128}
4129
4130fn regenerate_schema_from_snapshot(
4131 result: &mut IntrospectResult,
4132 dialect: Dialect,
4133 introspect_casing: Option<IntrospectCasing>,
4134) {
4135 if result.snapshot.dialect() != dialect.to_base() {
4136 return;
4137 }
4138 let generated = crate::codegen::schema_code(
4139 &result.snapshot,
4140 &crate::codegen::SchemaCodeOptions {
4141 casing: introspect_casing,
4142 module_doc: "Schema introspected from filtered database objects",
4143 schema_name: "Schema",
4144 },
4145 )
4146 .expect("initial MySQL introspection already validated lossless code generation");
4147
4148 result.schema_code = generated.code;
4149 result.table_count = generated.table_count;
4150 result.index_count = generated.index_count;
4151 result.view_count = generated.view_count;
4152 result.warnings = generated.warnings;
4153}
4154
4155fn introspect_database(connection: &ResolvedConnection) -> Result<IntrospectResult, CliError> {
4157 match connection.dialect {
4158 Dialect::Sqlite | Dialect::Turso => introspect_sqlite_dialect(connection),
4159 Dialect::Postgresql => introspect_postgres_dialect(connection),
4160 Dialect::Mysql => match &connection.credentials {
4161 Credentials::MySQL(creds) => mysql::introspect(connection.driver, creds),
4162 _ => Err(connection_driver_mismatch(connection)),
4163 },
4164 }
4165}
4166
4167fn introspect_sqlite_dialect(
4169 connection: &ResolvedConnection,
4170) -> Result<IntrospectResult, CliError> {
4171 match &connection.credentials {
4172 #[cfg(feature = "rusqlite")]
4173 Credentials::Sqlite { path } if connection.driver == Driver::Rusqlite => {
4174 introspect_rusqlite(path)
4175 }
4176
4177 #[cfg(feature = "rusqlite")]
4178 Credentials::Sqlite { .. } => Err(connection_driver_mismatch(connection)),
4179
4180 #[cfg(not(feature = "rusqlite"))]
4181 Credentials::Sqlite { .. } => Err(CliError::MissingDriver {
4182 dialect: "SQLite",
4183 feature: "rusqlite",
4184 }),
4185
4186 #[cfg(any(feature = "libsql", feature = "turso"))]
4187 Credentials::Turso { url, auth_token } => {
4188 if connection.driver == Driver::Libsql {
4189 #[cfg(feature = "libsql")]
4190 {
4191 introspect_libsql_local(url)
4192 }
4193 #[cfg(not(feature = "libsql"))]
4194 {
4195 Err(CliError::MissingDriver {
4196 dialect: "LibSQL (local)",
4197 feature: "libsql",
4198 })
4199 }
4200 } else if connection.driver == Driver::Turso {
4201 #[cfg(feature = "turso")]
4202 {
4203 introspect_turso(url, auth_token.as_deref())
4204 }
4205 #[cfg(not(feature = "turso"))]
4206 {
4207 let _ = auth_token;
4208 Err(CliError::MissingDriver {
4209 dialect: "Turso (remote)",
4210 feature: "turso",
4211 })
4212 }
4213 } else {
4214 Err(connection_driver_mismatch(connection))
4215 }
4216 }
4217
4218 #[cfg(all(not(feature = "turso"), not(feature = "libsql")))]
4219 Credentials::Turso { .. } => Err(CliError::MissingDriver {
4220 dialect: "Turso",
4221 feature: "turso or libsql",
4222 }),
4223
4224 _ => Err(CliError::Other(
4225 "SQLite introspection requires sqlite path or turso credentials".into(),
4226 )),
4227 }
4228}
4229
4230fn introspect_postgres_dialect(
4232 connection: &ResolvedConnection,
4233) -> Result<IntrospectResult, CliError> {
4234 match &connection.credentials {
4235 Credentials::Postgres(creds) => {
4236 let _ = creds;
4237 match connection.driver {
4238 Driver::PostgresSync => {
4239 #[cfg(feature = "postgres-sync")]
4240 {
4241 introspect_postgres_sync(creds)
4242 }
4243 #[cfg(not(feature = "postgres-sync"))]
4244 {
4245 Err(CliError::MissingDriver {
4246 dialect: "PostgreSQL",
4247 feature: "postgres-sync",
4248 })
4249 }
4250 }
4251 Driver::TokioPostgres => {
4252 #[cfg(feature = "tokio-postgres")]
4253 {
4254 introspect_postgres_async(creds)
4255 }
4256 #[cfg(not(feature = "tokio-postgres"))]
4257 {
4258 Err(CliError::MissingDriver {
4259 dialect: "PostgreSQL",
4260 feature: "tokio-postgres",
4261 })
4262 }
4263 }
4264 _ => Err(connection_driver_mismatch(connection)),
4265 }
4266 }
4267
4268 _ => Err(CliError::Other(
4269 "PostgreSQL introspection requires postgres credentials".into(),
4270 )),
4271 }
4272}
4273
4274#[cfg(any(feature = "rusqlite", feature = "libsql", feature = "turso"))]
4279struct SqliteRawData {
4280 tables: Vec<(String, Option<String>)>,
4281 raw_columns: Vec<drizzle_migrations::sqlite::introspect::RawColumnInfo>,
4282 all_indexes: Vec<drizzle_migrations::sqlite::introspect::RawIndexInfo>,
4283 all_index_columns: Vec<drizzle_migrations::sqlite::introspect::RawIndexColumn>,
4284 all_fks: Vec<drizzle_migrations::sqlite::introspect::RawForeignKey>,
4285 all_views: Vec<drizzle_migrations::sqlite::introspect::RawViewInfo>,
4286 index_sql: Vec<(String, String)>,
4287}
4288
4289#[cfg(feature = "rusqlite")]
4290impl SqliteRawData {
4291 const fn empty() -> Self {
4292 Self {
4293 tables: Vec::new(),
4294 raw_columns: Vec::new(),
4295 all_indexes: Vec::new(),
4296 all_index_columns: Vec::new(),
4297 all_fks: Vec::new(),
4298 all_views: Vec::new(),
4299 index_sql: Vec::new(),
4300 }
4301 }
4302}
4303
4304#[cfg(feature = "rusqlite")]
4305fn query_rusqlite_tables_and_columns(
4306 conn: &rusqlite::Connection,
4307 raw: &mut SqliteRawData,
4308) -> Result<(), CliError> {
4309 use drizzle_migrations::sqlite::introspect::{RawColumnInfo, queries};
4310
4311 let mut tables_stmt = conn
4312 .prepare(queries::TABLES_QUERY)
4313 .map_err(|e| CliError::Other(format!("Failed to prepare tables query: {e}")))?;
4314
4315 raw.tables = tables_stmt
4316 .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
4317 .map_err(|e| CliError::Other(e.to_string()))?
4318 .collect::<Result<Vec<_>, _>>()
4319 .map_err(|e| CliError::Other(e.to_string()))?;
4320
4321 let mut columns_stmt = conn
4322 .prepare(queries::COLUMNS_QUERY)
4323 .map_err(|e| CliError::Other(format!("Failed to prepare columns query: {e}")))?;
4324
4325 raw.raw_columns = columns_stmt
4326 .query_map([], |row| {
4327 Ok(RawColumnInfo {
4328 table: row.get(0)?,
4329 cid: row.get(1)?,
4330 name: row.get(2)?,
4331 column_type: row.get(3)?,
4332 not_null: row.get(4)?,
4333 default_value: row.get(5)?,
4334 pk: row.get(6)?,
4335 hidden: row.get(7)?,
4336 sql: row.get(8)?,
4337 })
4338 })
4339 .map_err(|e| CliError::Other(e.to_string()))?
4340 .collect::<Result<Vec<_>, _>>()
4341 .map_err(|e| CliError::Other(e.to_string()))?;
4342
4343 Ok(())
4344}
4345
4346#[cfg(feature = "rusqlite")]
4347fn query_rusqlite_indexes_and_foreign_keys(
4348 conn: &rusqlite::Connection,
4349 raw: &mut SqliteRawData,
4350) -> Result<(), CliError> {
4351 use drizzle_migrations::sqlite::introspect::{
4352 RawForeignKey, RawIndexColumn, RawIndexInfo, queries,
4353 };
4354
4355 let mut indexes_stmt = conn
4356 .prepare(queries::INDEXES_QUERY)
4357 .map_err(|e| CliError::Other(e.to_string()))?;
4358 raw.all_indexes = indexes_stmt
4359 .query_map([], |row| {
4360 Ok(RawIndexInfo {
4361 table: row.get(0)?,
4362 name: row.get(1)?,
4363 unique: row.get::<_, i32>(2)? != 0,
4364 origin: row.get(3)?,
4365 partial: row.get::<_, i32>(4)? != 0,
4366 })
4367 })
4368 .map_err(|e| CliError::Other(e.to_string()))?
4369 .collect::<Result<Vec<_>, _>>()
4370 .map_err(|e| CliError::Other(e.to_string()))?;
4371
4372 let mut columns_stmt = conn
4373 .prepare(queries::INDEX_COLUMNS_QUERY)
4374 .map_err(|e| CliError::Other(e.to_string()))?;
4375 raw.all_index_columns = columns_stmt
4376 .query_map([], |row| {
4377 Ok(RawIndexColumn {
4378 index_name: row.get(0)?,
4379 seqno: row.get(1)?,
4380 cid: row.get(2)?,
4381 name: row.get(3)?,
4382 desc: row.get::<_, i32>(4)? != 0,
4383 coll: row.get(5)?,
4384 key: row.get::<_, i32>(6)? != 0,
4385 })
4386 })
4387 .map_err(|e| CliError::Other(e.to_string()))?
4388 .collect::<Result<Vec<_>, _>>()
4389 .map_err(|e| CliError::Other(e.to_string()))?;
4390
4391 let mut foreign_keys_stmt = conn
4392 .prepare(queries::FOREIGN_KEYS_QUERY)
4393 .map_err(|e| CliError::Other(e.to_string()))?;
4394 raw.all_fks = foreign_keys_stmt
4395 .query_map([], |row| {
4396 Ok(RawForeignKey {
4397 table: row.get(0)?,
4398 id: row.get(1)?,
4399 seq: row.get(2)?,
4400 to_table: row.get(3)?,
4401 from_column: row.get(4)?,
4402 to_column: row.get(5)?,
4403 on_update: row.get(6)?,
4404 on_delete: row.get(7)?,
4405 r#match: row.get(8)?,
4406 })
4407 })
4408 .map_err(|e| CliError::Other(e.to_string()))?
4409 .collect::<Result<Vec<_>, _>>()
4410 .map_err(|e| CliError::Other(e.to_string()))?;
4411
4412 Ok(())
4413}
4414
4415#[cfg(feature = "rusqlite")]
4416fn query_rusqlite_views_and_view_columns(
4417 conn: &rusqlite::Connection,
4418 raw: &mut SqliteRawData,
4419) -> Result<(), CliError> {
4420 use drizzle_migrations::sqlite::introspect::{RawColumnInfo, RawViewInfo, queries};
4421
4422 let mut views_stmt = conn
4423 .prepare(queries::VIEWS_QUERY)
4424 .map_err(|e| CliError::Other(e.to_string()))?;
4425 let view_iter = views_stmt
4426 .query_map([], |row| {
4427 Ok(RawViewInfo {
4428 name: row.get(0)?,
4429 sql: row.get(1)?,
4430 })
4431 })
4432 .map_err(|e| CliError::Other(e.to_string()))?;
4433 raw.all_views.extend(
4434 view_iter
4435 .collect::<Result<Vec<_>, _>>()
4436 .map_err(|e| CliError::Other(e.to_string()))?,
4437 );
4438
4439 let mut view_cols_stmt = conn
4440 .prepare(queries::VIEW_COLUMNS_QUERY)
4441 .map_err(|e| CliError::Other(e.to_string()))?;
4442 let col_iter = view_cols_stmt
4443 .query_map([], |row| {
4444 Ok(RawColumnInfo {
4445 table: row.get(0)?,
4446 cid: row.get(1)?,
4447 name: row.get(2)?,
4448 column_type: row.get(3)?,
4449 not_null: row.get::<_, i32>(4)? != 0,
4450 default_value: row.get(5)?,
4451 pk: row.get(6)?,
4452 hidden: row.get(7)?,
4453 sql: row.get(8)?,
4454 })
4455 })
4456 .map_err(|e| CliError::Other(e.to_string()))?;
4457 raw.raw_columns.extend(
4458 col_iter
4459 .collect::<Result<Vec<_>, _>>()
4460 .map_err(|e| CliError::Other(e.to_string()))?,
4461 );
4462 Ok(())
4463}
4464
4465#[cfg(feature = "rusqlite")]
4466fn query_rusqlite_raw(conn: &rusqlite::Connection) -> Result<SqliteRawData, CliError> {
4467 use drizzle_migrations::sqlite::introspect::queries;
4468
4469 let mut raw = SqliteRawData::empty();
4470 query_rusqlite_tables_and_columns(conn, &mut raw)?;
4471 query_rusqlite_indexes_and_foreign_keys(conn, &mut raw)?;
4472 query_rusqlite_views_and_view_columns(conn, &mut raw)?;
4473
4474 let mut index_sql_stmt = conn
4475 .prepare(queries::INDEX_SQL_QUERY)
4476 .map_err(|e| CliError::Other(format!("Failed to prepare index sql query: {e}")))?;
4477 raw.index_sql = index_sql_stmt
4478 .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
4479 .map_err(|e| CliError::Other(e.to_string()))?
4480 .collect::<Result<Vec<_>, _>>()
4481 .map_err(|e| CliError::Other(e.to_string()))?;
4482
4483 Ok(raw)
4484}
4485
4486#[cfg(any(feature = "rusqlite", feature = "libsql", feature = "turso"))]
4487fn build_sqlite_ddl(raw: SqliteRawData) -> drizzle_migrations::sqlite::SQLiteDDL {
4488 drizzle_migrations::sqlite::introspect::assemble_ddl(
4489 drizzle_migrations::sqlite::introspect::RawIntrospection {
4490 tables: raw.tables,
4491 columns: raw.raw_columns,
4492 indexes: raw.all_indexes,
4493 index_columns: raw.all_index_columns,
4494 foreign_keys: raw.all_fks,
4495 views: raw.all_views,
4496 index_sql: raw.index_sql,
4497 },
4498 )
4499}
4500
4501#[cfg(feature = "rusqlite")]
4502fn introspect_rusqlite(path: &str) -> Result<IntrospectResult, CliError> {
4503 use drizzle_migrations::sqlite::codegen::{CodegenOptions, FieldCasing, generate_rust_schema};
4504
4505 let conn = rusqlite::Connection::open(path).map_err(|e| {
4506 CliError::ConnectionError(format!("Failed to open SQLite database '{path}': {e}"))
4507 })?;
4508
4509 let raw = query_rusqlite_raw(&conn)?;
4510 let ddl = build_sqlite_ddl(raw);
4511
4512 let options = CodegenOptions {
4513 module_doc: Some(format!("Schema introspected from {path}")),
4514 include_schema: true,
4515 schema_name: "Schema".to_string(),
4516 use_pub: true,
4517 field_casing: FieldCasing::default(),
4518 };
4519
4520 let generated = generate_rust_schema(&ddl, &options);
4521
4522 let mut sqlite_snapshot = drizzle_migrations::sqlite::SQLiteSnapshot::new();
4523 for entity in ddl.to_entities() {
4524 sqlite_snapshot.add_entity(entity);
4525 }
4526 let snapshot = Snapshot::Sqlite(sqlite_snapshot);
4527
4528 Ok(IntrospectResult {
4529 schema_code: generated.code,
4530 table_count: generated.tables.len(),
4531 index_count: generated.indexes.len(),
4532 view_count: ddl.views.len(),
4533 warnings: generated.warnings,
4534 mysql_catalog_defaults: None,
4535 snapshot,
4536 snapshot_path: std::path::PathBuf::new(),
4537 })
4538}
4539
4540#[cfg(feature = "libsql")]
4545fn introspect_libsql_local(path: &str) -> Result<IntrospectResult, CliError> {
4546 let rt = tokio::runtime::Builder::new_current_thread()
4547 .enable_all()
4548 .build()
4549 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
4550
4551 rt.block_on(introspect_libsql_inner(path, None))
4552}
4553
4554#[cfg(any(feature = "libsql", feature = "turso"))]
4555async fn query_libsql_tables_and_columns(
4556 conn: &libsql::Connection,
4557) -> Result<
4558 (
4559 Vec<(String, Option<String>)>,
4560 Vec<drizzle_migrations::sqlite::introspect::RawColumnInfo>,
4561 ),
4562 CliError,
4563> {
4564 use drizzle_migrations::sqlite::introspect::{RawColumnInfo, queries};
4565
4566 let mut tables_rows = conn
4567 .query(queries::TABLES_QUERY, ())
4568 .await
4569 .map_err(|e| CliError::Other(format!("Failed to query tables: {e}")))?;
4570
4571 let mut tables: Vec<(String, Option<String>)> = Vec::new();
4572 while let Some(row) = tables_rows
4573 .next()
4574 .await
4575 .map_err(|e| CliError::Other(e.to_string()))?
4576 {
4577 let name: String = row.get(0).map_err(|e| CliError::Other(e.to_string()))?;
4578 let sql: Option<String> = row.get(1).ok();
4579 tables.push((name, sql));
4580 }
4581
4582 let mut columns_rows = conn
4583 .query(queries::COLUMNS_QUERY, ())
4584 .await
4585 .map_err(|e| CliError::Other(format!("Failed to query columns: {e}")))?;
4586
4587 let mut raw_columns: Vec<RawColumnInfo> = Vec::new();
4588 while let Some(row) = columns_rows
4589 .next()
4590 .await
4591 .map_err(|e| CliError::Other(e.to_string()))?
4592 {
4593 raw_columns.push(RawColumnInfo {
4594 table: row.get(0).map_err(|e| CliError::Other(e.to_string()))?,
4595 cid: row.get(1).map_err(|e| CliError::Other(e.to_string()))?,
4596 name: row.get(2).map_err(|e| CliError::Other(e.to_string()))?,
4597 column_type: row.get(3).map_err(|e| CliError::Other(e.to_string()))?,
4598 not_null: row
4599 .get::<i32>(4)
4600 .map_err(|e| CliError::Other(e.to_string()))?
4601 != 0,
4602 default_value: row.get(5).ok(),
4603 pk: row.get(6).map_err(|e| CliError::Other(e.to_string()))?,
4604 hidden: row.get(7).map_err(|e| CliError::Other(e.to_string()))?,
4605 sql: row.get(8).ok(),
4606 });
4607 }
4608
4609 Ok((tables, raw_columns))
4610}
4611
4612#[cfg(any(feature = "libsql", feature = "turso"))]
4613async fn query_libsql_indexes_and_foreign_keys(
4614 conn: &libsql::Connection,
4615) -> Result<
4616 (
4617 Vec<drizzle_migrations::sqlite::introspect::RawIndexInfo>,
4618 Vec<drizzle_migrations::sqlite::introspect::RawIndexColumn>,
4619 Vec<drizzle_migrations::sqlite::introspect::RawForeignKey>,
4620 ),
4621 CliError,
4622> {
4623 use drizzle_migrations::sqlite::introspect::{
4624 RawForeignKey, RawIndexColumn, RawIndexInfo, queries,
4625 };
4626
4627 let mut all_indexes = Vec::<RawIndexInfo>::new();
4628 let mut index_rows = conn
4629 .query(queries::INDEXES_QUERY, ())
4630 .await
4631 .map_err(|e| CliError::Other(e.to_string()))?;
4632 while let Some(row) = index_rows
4633 .next()
4634 .await
4635 .map_err(|e| CliError::Other(e.to_string()))?
4636 {
4637 all_indexes.push(RawIndexInfo {
4638 table: row.get(0).map_err(|e| CliError::Other(e.to_string()))?,
4639 name: row.get(1).map_err(|e| CliError::Other(e.to_string()))?,
4640 unique: row
4641 .get::<i32>(2)
4642 .map_err(|e| CliError::Other(e.to_string()))?
4643 != 0,
4644 origin: row.get(3).map_err(|e| CliError::Other(e.to_string()))?,
4645 partial: row
4646 .get::<i32>(4)
4647 .map_err(|e| CliError::Other(e.to_string()))?
4648 != 0,
4649 });
4650 }
4651
4652 let mut all_index_columns = Vec::<RawIndexColumn>::new();
4653 let mut column_rows = conn
4654 .query(queries::INDEX_COLUMNS_QUERY, ())
4655 .await
4656 .map_err(|e| CliError::Other(e.to_string()))?;
4657 while let Some(row) = column_rows
4658 .next()
4659 .await
4660 .map_err(|e| CliError::Other(e.to_string()))?
4661 {
4662 all_index_columns.push(RawIndexColumn {
4663 index_name: row.get(0).map_err(|e| CliError::Other(e.to_string()))?,
4664 seqno: row.get(1).map_err(|e| CliError::Other(e.to_string()))?,
4665 cid: row.get(2).map_err(|e| CliError::Other(e.to_string()))?,
4666 name: row.get(3).ok(),
4667 desc: row
4668 .get::<i32>(4)
4669 .map_err(|e| CliError::Other(e.to_string()))?
4670 != 0,
4671 coll: row.get(5).map_err(|e| CliError::Other(e.to_string()))?,
4672 key: row
4673 .get::<i32>(6)
4674 .map_err(|e| CliError::Other(e.to_string()))?
4675 != 0,
4676 });
4677 }
4678
4679 let mut all_fks = Vec::<RawForeignKey>::new();
4680 let mut foreign_key_rows = conn
4681 .query(queries::FOREIGN_KEYS_QUERY, ())
4682 .await
4683 .map_err(|e| CliError::Other(e.to_string()))?;
4684 while let Some(row) = foreign_key_rows
4685 .next()
4686 .await
4687 .map_err(|e| CliError::Other(e.to_string()))?
4688 {
4689 all_fks.push(RawForeignKey {
4690 table: row.get(0).map_err(|e| CliError::Other(e.to_string()))?,
4691 id: row.get(1).map_err(|e| CliError::Other(e.to_string()))?,
4692 seq: row.get(2).map_err(|e| CliError::Other(e.to_string()))?,
4693 to_table: row.get(3).map_err(|e| CliError::Other(e.to_string()))?,
4694 from_column: row.get(4).map_err(|e| CliError::Other(e.to_string()))?,
4695 to_column: row.get(5).map_err(|e| CliError::Other(e.to_string()))?,
4696 on_update: row.get(6).map_err(|e| CliError::Other(e.to_string()))?,
4697 on_delete: row.get(7).map_err(|e| CliError::Other(e.to_string()))?,
4698 r#match: row.get(8).map_err(|e| CliError::Other(e.to_string()))?,
4699 });
4700 }
4701
4702 Ok((all_indexes, all_index_columns, all_fks))
4703}
4704
4705#[cfg(any(feature = "libsql", feature = "turso"))]
4706async fn query_libsql_views_and_view_columns(
4707 conn: &libsql::Connection,
4708 raw_columns: &mut Vec<drizzle_migrations::sqlite::introspect::RawColumnInfo>,
4709) -> Result<Vec<drizzle_migrations::sqlite::introspect::RawViewInfo>, CliError> {
4710 use drizzle_migrations::sqlite::introspect::{RawColumnInfo, RawViewInfo, queries};
4711
4712 let mut all_views: Vec<RawViewInfo> = Vec::new();
4713 let mut views_rows = conn
4714 .query(queries::VIEWS_QUERY, ())
4715 .await
4716 .map_err(|e| CliError::Other(e.to_string()))?;
4717 while let Some(row) = views_rows
4718 .next()
4719 .await
4720 .map_err(|e| CliError::Other(e.to_string()))?
4721 {
4722 let name: String = row.get(0).map_err(|e| CliError::Other(e.to_string()))?;
4723 let sql: String = row.get(1).map_err(|e| CliError::Other(e.to_string()))?;
4724 all_views.push(RawViewInfo { name, sql });
4725 }
4726
4727 let mut view_cols_rows = conn
4728 .query(queries::VIEW_COLUMNS_QUERY, ())
4729 .await
4730 .map_err(|e| CliError::Other(e.to_string()))?;
4731 while let Some(row) = view_cols_rows
4732 .next()
4733 .await
4734 .map_err(|e| CliError::Other(e.to_string()))?
4735 {
4736 raw_columns.push(RawColumnInfo {
4737 table: row.get(0).map_err(|e| CliError::Other(e.to_string()))?,
4738 cid: row.get(1).map_err(|e| CliError::Other(e.to_string()))?,
4739 name: row.get(2).map_err(|e| CliError::Other(e.to_string()))?,
4740 column_type: row.get(3).map_err(|e| CliError::Other(e.to_string()))?,
4741 not_null: row
4742 .get::<i32>(4)
4743 .map_err(|e| CliError::Other(e.to_string()))?
4744 != 0,
4745 default_value: row.get(5).ok(),
4746 pk: row.get(6).map_err(|e| CliError::Other(e.to_string()))?,
4747 hidden: row.get(7).map_err(|e| CliError::Other(e.to_string()))?,
4748 sql: row.get(8).ok(),
4749 });
4750 }
4751 Ok(all_views)
4752}
4753
4754#[cfg(any(feature = "libsql", feature = "turso"))]
4755async fn query_libsql_raw(conn: &libsql::Connection) -> Result<SqliteRawData, CliError> {
4756 use drizzle_migrations::sqlite::introspect::queries;
4757
4758 let (tables, mut raw_columns) = query_libsql_tables_and_columns(conn).await?;
4759 let (all_indexes, all_index_columns, all_fks) =
4760 query_libsql_indexes_and_foreign_keys(conn).await?;
4761 let all_views = query_libsql_views_and_view_columns(conn, &mut raw_columns).await?;
4762
4763 let mut index_sql = Vec::new();
4764 let mut rows = conn
4765 .query(queries::INDEX_SQL_QUERY, ())
4766 .await
4767 .map_err(|e| CliError::Other(e.to_string()))?;
4768 while let Some(row) = rows
4769 .next()
4770 .await
4771 .map_err(|e| CliError::Other(e.to_string()))?
4772 {
4773 let name: String = row.get(0).map_err(|e| CliError::Other(e.to_string()))?;
4774 let sql: String = row.get(1).map_err(|e| CliError::Other(e.to_string()))?;
4775 index_sql.push((name, sql));
4776 }
4777
4778 Ok(SqliteRawData {
4779 tables,
4780 raw_columns,
4781 all_indexes,
4782 all_index_columns,
4783 all_fks,
4784 all_views,
4785 index_sql,
4786 })
4787}
4788
4789#[cfg(feature = "libsql")]
4790async fn introspect_libsql_inner(
4791 path: &str,
4792 _auth_token: Option<&str>,
4793) -> Result<IntrospectResult, CliError> {
4794 use drizzle_migrations::sqlite::codegen::{CodegenOptions, FieldCasing, generate_rust_schema};
4795
4796 let db = libsql::Builder::new_local(path)
4797 .build()
4798 .await
4799 .map_err(|e| {
4800 CliError::ConnectionError(format!("Failed to open LibSQL database '{path}': {e}"))
4801 })?;
4802
4803 let conn = db
4804 .connect()
4805 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
4806
4807 let raw = query_libsql_raw(&conn).await?;
4808 let ddl = build_sqlite_ddl(raw);
4809
4810 let options = CodegenOptions {
4811 module_doc: Some(format!("Schema introspected from {path}")),
4812 include_schema: true,
4813 schema_name: "Schema".to_string(),
4814 use_pub: true,
4815 field_casing: FieldCasing::default(),
4816 };
4817
4818 let generated = generate_rust_schema(&ddl, &options);
4819
4820 let mut sqlite_snapshot = drizzle_migrations::sqlite::SQLiteSnapshot::new();
4821 for entity in ddl.to_entities() {
4822 sqlite_snapshot.add_entity(entity);
4823 }
4824 let snapshot = Snapshot::Sqlite(sqlite_snapshot);
4825
4826 Ok(IntrospectResult {
4827 schema_code: generated.code,
4828 table_count: generated.tables.len(),
4829 index_count: generated.indexes.len(),
4830 view_count: ddl.views.len(),
4831 warnings: generated.warnings,
4832 mysql_catalog_defaults: None,
4833 snapshot,
4834 snapshot_path: std::path::PathBuf::new(),
4835 })
4836}
4837
4838#[cfg(feature = "turso")]
4843fn introspect_turso(url: &str, auth_token: Option<&str>) -> Result<IntrospectResult, CliError> {
4844 let rt = tokio::runtime::Builder::new_current_thread()
4845 .enable_all()
4846 .build()
4847 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
4848
4849 rt.block_on(introspect_turso_inner(url, auth_token))
4850}
4851
4852#[cfg(feature = "turso")]
4853async fn introspect_turso_inner(
4854 url: &str,
4855 auth_token: Option<&str>,
4856) -> Result<IntrospectResult, CliError> {
4857 use drizzle_migrations::sqlite::codegen::{CodegenOptions, FieldCasing, generate_rust_schema};
4858
4859 let builder =
4860 libsql::Builder::new_remote(url.to_string(), auth_token.unwrap_or("").to_string());
4861
4862 let db = builder.build().await.map_err(|e| {
4863 CliError::ConnectionError(format!("Failed to connect to Turso '{url}': {e}"))
4864 })?;
4865
4866 let conn = db
4867 .connect()
4868 .map_err(|e| CliError::ConnectionError(e.to_string()))?;
4869
4870 let raw = query_libsql_raw(&conn).await?;
4871 let ddl = build_sqlite_ddl(raw);
4872
4873 let options = CodegenOptions {
4874 module_doc: Some(format!("Schema introspected from Turso: {url}")),
4875 include_schema: true,
4876 schema_name: "Schema".to_string(),
4877 use_pub: true,
4878 field_casing: FieldCasing::default(),
4879 };
4880
4881 let generated = generate_rust_schema(&ddl, &options);
4882
4883 let mut sqlite_snapshot = drizzle_migrations::sqlite::SQLiteSnapshot::new();
4884 for entity in ddl.to_entities() {
4885 sqlite_snapshot.add_entity(entity);
4886 }
4887 let snapshot = Snapshot::Sqlite(sqlite_snapshot);
4888
4889 Ok(IntrospectResult {
4890 schema_code: generated.code,
4891 table_count: generated.tables.len(),
4892 index_count: generated.indexes.len(),
4893 view_count: ddl.views.len(),
4894 warnings: generated.warnings,
4895 mysql_catalog_defaults: None,
4896 snapshot,
4897 snapshot_path: std::path::PathBuf::new(),
4898 })
4899}
4900
4901#[cfg(feature = "postgres-sync")]
4906fn introspect_postgres_sync(creds: &PostgresCreds) -> Result<IntrospectResult, CliError> {
4907 let mut client = connect_postgres_sync(creds)?;
4908
4909 let raw = query_postgres_sync_raw(&mut client)?;
4910 let ddl = build_postgres_ddl(raw);
4911
4912 Ok(finalize_postgres_introspection(
4913 &ddl,
4914 "configured PostgreSQL database",
4915 ))
4916}
4917
4918#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
4919impl PostgresRawData {
4920 const fn empty() -> Self {
4921 Self {
4922 schemas: Vec::new(),
4923 tables: Vec::new(),
4924 columns: Vec::new(),
4925 enums: Vec::new(),
4926 sequences: Vec::new(),
4927 views: Vec::new(),
4928 indexes: Vec::new(),
4929 foreign_keys: Vec::new(),
4930 primary_keys: Vec::new(),
4931 uniques: Vec::new(),
4932 checks: Vec::new(),
4933 roles: Vec::new(),
4934 policies: Vec::new(),
4935 }
4936 }
4937}
4938
4939#[cfg(feature = "postgres-sync")]
4940fn query_postgres_sync_raw(client: &mut postgres::Client) -> Result<PostgresRawData, CliError> {
4941 let mut raw = PostgresRawData::empty();
4942 query_pg_sync_core(client, &mut raw)?;
4943 query_pg_sync_codegen_meta(client, &mut raw)?;
4944 query_pg_sync_constraints(client, &mut raw)?;
4945 query_pg_sync_security(client, &mut raw)?;
4946 Ok(raw)
4947}
4948
4949#[cfg(feature = "postgres-sync")]
4950fn query_pg_sync_core(
4951 client: &mut postgres::Client,
4952 raw: &mut PostgresRawData,
4953) -> Result<(), CliError> {
4954 use drizzle_migrations::postgres::introspect::{RawColumnInfo, RawTableInfo, queries};
4955
4956 raw.schemas = client
4957 .query(queries::SCHEMAS_QUERY, &[])
4958 .map_err(|e| CliError::Other(format!("Failed to query schemas: {e}")))?
4959 .into_iter()
4960 .map(|row| RawSchemaInfo {
4961 name: row.get::<_, String>(0),
4962 })
4963 .collect();
4964
4965 raw.tables = client
4966 .query(queries::TABLES_QUERY, &[])
4967 .map_err(|e| CliError::Other(format!("Failed to query tables: {e}")))?
4968 .into_iter()
4969 .map(|row| RawTableInfo {
4970 schema: row.get::<_, String>(0),
4971 name: row.get::<_, String>(1),
4972 is_rls_enabled: row.get::<_, bool>(2),
4973 is_unlogged: row.get::<_, bool>(3),
4974 is_temporary: row.get::<_, bool>(4),
4975 tablespace: row.get::<_, Option<String>>(5),
4976 comment: row.get::<_, Option<String>>(6),
4977 })
4978 .collect();
4979
4980 raw.columns = client
4981 .query(queries::COLUMNS_QUERY, &[])
4982 .map_err(|e| CliError::Other(format!("Failed to query columns: {e}")))?
4983 .into_iter()
4984 .map(|row| RawColumnInfo {
4985 schema: row.get::<_, String>(0),
4986 table: row.get::<_, String>(1),
4987 name: row.get::<_, String>(2),
4988 column_type: row.get::<_, String>(3),
4989 type_schema: row.get::<_, Option<String>>(4),
4990 not_null: row.get::<_, bool>(5),
4991 default_value: row.get::<_, Option<String>>(6),
4992 is_identity: row.get::<_, bool>(7),
4993 identity_type: row.get::<_, Option<String>>(8),
4994 is_generated: row.get::<_, bool>(9),
4995 generated_expression: row.get::<_, Option<String>>(10),
4996 generated_stored: row.get::<_, bool>(11),
4997 dimensions: row.get::<_, Option<i32>>(12),
4998 comment: row.get::<_, Option<String>>(13),
4999 ordinal_position: row.get::<_, i32>(14),
5000 })
5001 .collect();
5002
5003 Ok(())
5004}
5005
5006#[cfg(feature = "postgres-sync")]
5007fn query_pg_sync_codegen_meta(
5008 client: &mut postgres::Client,
5009 raw: &mut PostgresRawData,
5010) -> Result<(), CliError> {
5011 use drizzle_migrations::postgres::introspect::{
5012 RawEnumInfo, RawSequenceInfo, RawViewInfo, queries,
5013 };
5014
5015 raw.enums = client
5016 .query(queries::ENUMS_QUERY, &[])
5017 .map_err(|e| CliError::Other(format!("Failed to query enums: {e}")))?
5018 .into_iter()
5019 .map(|row| RawEnumInfo {
5020 schema: row.get::<_, String>(0),
5021 name: row.get::<_, String>(1),
5022 values: row.get::<_, Vec<String>>(2),
5023 })
5024 .collect();
5025
5026 raw.sequences = client
5027 .query(queries::SEQUENCES_QUERY, &[])
5028 .map_err(|e| CliError::Other(format!("Failed to query sequences: {e}")))?
5029 .into_iter()
5030 .map(|row| RawSequenceInfo {
5031 schema: row.get::<_, String>(0),
5032 name: row.get::<_, String>(1),
5033 data_type: row.get::<_, Option<String>>(2),
5034 start_value: row.get::<_, Option<String>>(3),
5035 min_value: row.get::<_, Option<String>>(4),
5036 max_value: row.get::<_, Option<String>>(5),
5037 increment: row.get::<_, Option<String>>(6),
5038 cycle: row.get::<_, Option<bool>>(7),
5039 cache_value: row.get::<_, Option<String>>(8),
5040 owned_by: row.get::<_, Option<String>>(9),
5041 })
5042 .collect();
5043
5044 let view_schema_filters: Option<Vec<String>> = None;
5045 raw.views = client
5046 .query(queries::VIEWS_QUERY, &[&view_schema_filters])
5047 .map_err(|e| CliError::Other(format!("Failed to query views: {e}")))?
5048 .into_iter()
5049 .map(|row| RawViewInfo {
5050 schema: row.get::<_, String>(0),
5051 name: row.get::<_, String>(1),
5052 definition: row.get::<_, String>(2),
5053 is_materialized: row.get::<_, bool>(3),
5054 })
5055 .collect();
5056
5057 Ok(())
5058}
5059
5060#[cfg(feature = "postgres-sync")]
5061fn query_pg_sync_constraints(
5062 client: &mut postgres::Client,
5063 raw: &mut PostgresRawData,
5064) -> Result<(), CliError> {
5065 use drizzle_migrations::postgres::introspect::{
5066 RawCheckInfo, RawForeignKeyInfo, RawIndexInfo, RawPrimaryKeyInfo, RawUniqueInfo,
5067 action_code_to_string, parse_index_columns, queries,
5068 };
5069
5070 raw.indexes = client
5071 .query(queries::INDEXES_QUERY, &[])
5072 .map_err(|e| CliError::Other(format!("Failed to query indexes: {e}")))?
5073 .into_iter()
5074 .map(|row| {
5075 let cols: Vec<String> = row.get(6);
5076 RawIndexInfo {
5077 schema: row.get::<_, String>(0),
5078 table: row.get::<_, String>(1),
5079 name: row.get::<_, String>(2),
5080 is_unique: row.get::<_, bool>(3),
5081 is_primary: row.get::<_, bool>(4),
5082 method: row.get::<_, String>(5),
5083 columns: parse_index_columns(cols),
5084 where_clause: row.get::<_, Option<String>>(7),
5085 concurrent: false,
5086 }
5087 })
5088 .collect();
5089
5090 raw.foreign_keys = client
5091 .query(queries::FOREIGN_KEYS_QUERY, &[])
5092 .map_err(|e| CliError::Other(format!("Failed to query foreign keys: {e}")))?
5093 .into_iter()
5094 .map(|row| RawForeignKeyInfo {
5095 schema: row.get::<_, String>(0),
5096 table: row.get::<_, String>(1),
5097 name: row.get::<_, String>(2),
5098 columns: row.get::<_, Vec<String>>(3),
5099 schema_to: row.get::<_, String>(4),
5100 table_to: row.get::<_, String>(5),
5101 columns_to: row.get::<_, Vec<String>>(6),
5102 on_update: action_code_to_string(&row.get::<_, String>(7)),
5103 on_delete: action_code_to_string(&row.get::<_, String>(8)),
5104 deferrable: row.get::<_, bool>(9),
5105 initially_deferred: row.get::<_, bool>(10),
5106 })
5107 .collect();
5108
5109 raw.primary_keys = client
5110 .query(queries::PRIMARY_KEYS_QUERY, &[])
5111 .map_err(|e| CliError::Other(format!("Failed to query primary keys: {e}")))?
5112 .into_iter()
5113 .map(|row| RawPrimaryKeyInfo {
5114 schema: row.get::<_, String>(0),
5115 table: row.get::<_, String>(1),
5116 name: row.get::<_, String>(2),
5117 columns: row.get::<_, Vec<String>>(3),
5118 })
5119 .collect();
5120
5121 raw.uniques = client
5122 .query(queries::UNIQUES_QUERY, &[])
5123 .map_err(|e| CliError::Other(format!("Failed to query unique constraints: {e}")))?
5124 .into_iter()
5125 .map(|row| RawUniqueInfo {
5126 schema: row.get::<_, String>(0),
5127 table: row.get::<_, String>(1),
5128 name: row.get::<_, String>(2),
5129 columns: row.get::<_, Vec<String>>(3),
5130 nulls_not_distinct: row.get::<_, bool>(4),
5131 deferrable: row.get::<_, bool>(5),
5132 initially_deferred: row.get::<_, bool>(6),
5133 })
5134 .collect();
5135
5136 raw.checks = client
5137 .query(queries::CHECKS_QUERY, &[])
5138 .map_err(|e| CliError::Other(format!("Failed to query check constraints: {e}")))?
5139 .into_iter()
5140 .map(|row| RawCheckInfo {
5141 schema: row.get::<_, String>(0),
5142 table: row.get::<_, String>(1),
5143 name: row.get::<_, String>(2),
5144 expression: row.get::<_, String>(3),
5145 })
5146 .collect();
5147
5148 Ok(())
5149}
5150
5151#[cfg(feature = "postgres-sync")]
5152fn query_pg_sync_security(
5153 client: &mut postgres::Client,
5154 raw: &mut PostgresRawData,
5155) -> Result<(), CliError> {
5156 use drizzle_migrations::postgres::introspect::{RawPolicyInfo, RawRoleInfo, queries};
5157
5158 raw.roles = client
5159 .query(queries::ROLES_QUERY, &[])
5160 .map_err(|e| CliError::Other(format!("Failed to query roles: {e}")))?
5161 .into_iter()
5162 .map(|row| RawRoleInfo {
5163 name: row.get::<_, String>(0),
5164 create_db: row.get::<_, bool>(1),
5165 create_role: row.get::<_, bool>(2),
5166 inherit: row.get::<_, bool>(3),
5167 })
5168 .collect();
5169
5170 raw.policies = client
5171 .query(queries::POLICIES_QUERY, &[])
5172 .map_err(|e| CliError::Other(format!("Failed to query policies: {e}")))?
5173 .into_iter()
5174 .map(|row| RawPolicyInfo {
5175 schema: row.get::<_, String>(0),
5176 table: row.get::<_, String>(1),
5177 name: row.get::<_, String>(2),
5178 as_clause: row.get::<_, String>(3),
5179 for_clause: row.get::<_, String>(4),
5180 to: row.get::<_, Vec<String>>(5),
5181 using: row.get::<_, Option<String>>(6),
5182 with_check: row.get::<_, Option<String>>(7),
5183 })
5184 .collect();
5185
5186 Ok(())
5187}
5188
5189#[cfg(feature = "tokio-postgres")]
5190fn introspect_postgres_async(creds: &PostgresCreds) -> Result<IntrospectResult, CliError> {
5191 let rt = tokio::runtime::Builder::new_current_thread()
5192 .enable_all()
5193 .build()
5194 .map_err(|e| CliError::Other(format!("Failed to create async runtime: {e}")))?;
5195
5196 rt.block_on(introspect_postgres_async_inner(creds))
5197}
5198
5199#[cfg(feature = "tokio-postgres")]
5200async fn introspect_postgres_async_inner(
5201 creds: &PostgresCreds,
5202) -> Result<IntrospectResult, CliError> {
5203 let client = connect_postgres_async(creds).await?;
5204
5205 let raw = query_postgres_async_raw(&client).await?;
5206 let ddl = build_postgres_ddl(raw);
5207
5208 Ok(finalize_postgres_introspection(
5209 &ddl,
5210 "configured PostgreSQL database",
5211 ))
5212}
5213
5214#[cfg(feature = "tokio-postgres")]
5215async fn query_postgres_async_raw(
5216 client: &tokio_postgres::Client,
5217) -> Result<PostgresRawData, CliError> {
5218 let mut raw = PostgresRawData::empty();
5219 query_pg_async_core(client, &mut raw).await?;
5220 query_pg_async_codegen_meta(client, &mut raw).await?;
5221 query_pg_async_constraints(client, &mut raw).await?;
5222 query_pg_async_security(client, &mut raw).await?;
5223 Ok(raw)
5224}
5225
5226#[cfg(feature = "tokio-postgres")]
5227async fn query_pg_async_core(
5228 client: &tokio_postgres::Client,
5229 raw: &mut PostgresRawData,
5230) -> Result<(), CliError> {
5231 use drizzle_migrations::postgres::introspect::{RawColumnInfo, RawTableInfo, queries};
5232
5233 raw.schemas = client
5234 .query(queries::SCHEMAS_QUERY, &[])
5235 .await
5236 .map_err(|e| CliError::Other(format!("Failed to query schemas: {e}")))?
5237 .into_iter()
5238 .map(|row| RawSchemaInfo {
5239 name: row.get::<_, String>(0),
5240 })
5241 .collect();
5242
5243 raw.tables = client
5244 .query(queries::TABLES_QUERY, &[])
5245 .await
5246 .map_err(|e| CliError::Other(format!("Failed to query tables: {e}")))?
5247 .into_iter()
5248 .map(|row| RawTableInfo {
5249 schema: row.get::<_, String>(0),
5250 name: row.get::<_, String>(1),
5251 is_rls_enabled: row.get::<_, bool>(2),
5252 is_unlogged: row.get::<_, bool>(3),
5253 is_temporary: row.get::<_, bool>(4),
5254 tablespace: row.get::<_, Option<String>>(5),
5255 comment: row.get::<_, Option<String>>(6),
5256 })
5257 .collect();
5258
5259 raw.columns = client
5260 .query(queries::COLUMNS_QUERY, &[])
5261 .await
5262 .map_err(|e| CliError::Other(format!("Failed to query columns: {e}")))?
5263 .into_iter()
5264 .map(|row| RawColumnInfo {
5265 schema: row.get::<_, String>(0),
5266 table: row.get::<_, String>(1),
5267 name: row.get::<_, String>(2),
5268 column_type: row.get::<_, String>(3),
5269 type_schema: row.get::<_, Option<String>>(4),
5270 not_null: row.get::<_, bool>(5),
5271 default_value: row.get::<_, Option<String>>(6),
5272 is_identity: row.get::<_, bool>(7),
5273 identity_type: row.get::<_, Option<String>>(8),
5274 is_generated: row.get::<_, bool>(9),
5275 generated_expression: row.get::<_, Option<String>>(10),
5276 generated_stored: row.get::<_, bool>(11),
5277 dimensions: row.get::<_, Option<i32>>(12),
5278 comment: row.get::<_, Option<String>>(13),
5279 ordinal_position: row.get::<_, i32>(14),
5280 })
5281 .collect();
5282
5283 Ok(())
5284}
5285
5286#[cfg(feature = "tokio-postgres")]
5287async fn query_pg_async_codegen_meta(
5288 client: &tokio_postgres::Client,
5289 raw: &mut PostgresRawData,
5290) -> Result<(), CliError> {
5291 use drizzle_migrations::postgres::introspect::{
5292 RawEnumInfo, RawSequenceInfo, RawViewInfo, queries,
5293 };
5294
5295 raw.enums = client
5296 .query(queries::ENUMS_QUERY, &[])
5297 .await
5298 .map_err(|e| CliError::Other(format!("Failed to query enums: {e}")))?
5299 .into_iter()
5300 .map(|row| RawEnumInfo {
5301 schema: row.get::<_, String>(0),
5302 name: row.get::<_, String>(1),
5303 values: row.get::<_, Vec<String>>(2),
5304 })
5305 .collect();
5306
5307 raw.sequences = client
5308 .query(queries::SEQUENCES_QUERY, &[])
5309 .await
5310 .map_err(|e| CliError::Other(format!("Failed to query sequences: {e}")))?
5311 .into_iter()
5312 .map(|row| RawSequenceInfo {
5313 schema: row.get::<_, String>(0),
5314 name: row.get::<_, String>(1),
5315 data_type: row.get::<_, Option<String>>(2),
5316 start_value: row.get::<_, Option<String>>(3),
5317 min_value: row.get::<_, Option<String>>(4),
5318 max_value: row.get::<_, Option<String>>(5),
5319 increment: row.get::<_, Option<String>>(6),
5320 cycle: row.get::<_, Option<bool>>(7),
5321 cache_value: row.get::<_, Option<String>>(8),
5322 owned_by: row.get::<_, Option<String>>(9),
5323 })
5324 .collect();
5325
5326 let view_schema_filters: Option<Vec<String>> = None;
5327 raw.views = client
5328 .query(queries::VIEWS_QUERY, &[&view_schema_filters])
5329 .await
5330 .map_err(|e| CliError::Other(format!("Failed to query views: {e}")))?
5331 .into_iter()
5332 .map(|row| RawViewInfo {
5333 schema: row.get::<_, String>(0),
5334 name: row.get::<_, String>(1),
5335 definition: row.get::<_, String>(2),
5336 is_materialized: row.get::<_, bool>(3),
5337 })
5338 .collect();
5339
5340 Ok(())
5341}
5342
5343#[cfg(feature = "tokio-postgres")]
5344async fn query_pg_async_constraints(
5345 client: &tokio_postgres::Client,
5346 raw: &mut PostgresRawData,
5347) -> Result<(), CliError> {
5348 use drizzle_migrations::postgres::introspect::{
5349 RawCheckInfo, RawForeignKeyInfo, RawIndexInfo, RawPrimaryKeyInfo, RawUniqueInfo,
5350 action_code_to_string, parse_index_columns, queries,
5351 };
5352
5353 raw.indexes = client
5354 .query(queries::INDEXES_QUERY, &[])
5355 .await
5356 .map_err(|e| CliError::Other(format!("Failed to query indexes: {e}")))?
5357 .into_iter()
5358 .map(|row| {
5359 let cols: Vec<String> = row.get(6);
5360 RawIndexInfo {
5361 schema: row.get::<_, String>(0),
5362 table: row.get::<_, String>(1),
5363 name: row.get::<_, String>(2),
5364 is_unique: row.get::<_, bool>(3),
5365 is_primary: row.get::<_, bool>(4),
5366 method: row.get::<_, String>(5),
5367 columns: parse_index_columns(cols),
5368 where_clause: row.get::<_, Option<String>>(7),
5369 concurrent: false,
5370 }
5371 })
5372 .collect();
5373
5374 raw.foreign_keys = client
5375 .query(queries::FOREIGN_KEYS_QUERY, &[])
5376 .await
5377 .map_err(|e| CliError::Other(format!("Failed to query foreign keys: {e}")))?
5378 .into_iter()
5379 .map(|row| RawForeignKeyInfo {
5380 schema: row.get::<_, String>(0),
5381 table: row.get::<_, String>(1),
5382 name: row.get::<_, String>(2),
5383 columns: row.get::<_, Vec<String>>(3),
5384 schema_to: row.get::<_, String>(4),
5385 table_to: row.get::<_, String>(5),
5386 columns_to: row.get::<_, Vec<String>>(6),
5387 on_update: action_code_to_string(&row.get::<_, String>(7)),
5388 on_delete: action_code_to_string(&row.get::<_, String>(8)),
5389 deferrable: row.get::<_, bool>(9),
5390 initially_deferred: row.get::<_, bool>(10),
5391 })
5392 .collect();
5393
5394 raw.primary_keys = client
5395 .query(queries::PRIMARY_KEYS_QUERY, &[])
5396 .await
5397 .map_err(|e| CliError::Other(format!("Failed to query primary keys: {e}")))?
5398 .into_iter()
5399 .map(|row| RawPrimaryKeyInfo {
5400 schema: row.get::<_, String>(0),
5401 table: row.get::<_, String>(1),
5402 name: row.get::<_, String>(2),
5403 columns: row.get::<_, Vec<String>>(3),
5404 })
5405 .collect();
5406
5407 raw.uniques = client
5408 .query(queries::UNIQUES_QUERY, &[])
5409 .await
5410 .map_err(|e| CliError::Other(format!("Failed to query unique constraints: {e}")))?
5411 .into_iter()
5412 .map(|row| RawUniqueInfo {
5413 schema: row.get::<_, String>(0),
5414 table: row.get::<_, String>(1),
5415 name: row.get::<_, String>(2),
5416 columns: row.get::<_, Vec<String>>(3),
5417 nulls_not_distinct: row.get::<_, bool>(4),
5418 deferrable: row.get::<_, bool>(5),
5419 initially_deferred: row.get::<_, bool>(6),
5420 })
5421 .collect();
5422
5423 raw.checks = client
5424 .query(queries::CHECKS_QUERY, &[])
5425 .await
5426 .map_err(|e| CliError::Other(format!("Failed to query check constraints: {e}")))?
5427 .into_iter()
5428 .map(|row| RawCheckInfo {
5429 schema: row.get::<_, String>(0),
5430 table: row.get::<_, String>(1),
5431 name: row.get::<_, String>(2),
5432 expression: row.get::<_, String>(3),
5433 })
5434 .collect();
5435
5436 Ok(())
5437}
5438
5439#[cfg(feature = "tokio-postgres")]
5440async fn query_pg_async_security(
5441 client: &tokio_postgres::Client,
5442 raw: &mut PostgresRawData,
5443) -> Result<(), CliError> {
5444 use drizzle_migrations::postgres::introspect::{RawPolicyInfo, RawRoleInfo, queries};
5445
5446 raw.roles = client
5447 .query(queries::ROLES_QUERY, &[])
5448 .await
5449 .map_err(|e| CliError::Other(format!("Failed to query roles: {e}")))?
5450 .into_iter()
5451 .map(|row| RawRoleInfo {
5452 name: row.get::<_, String>(0),
5453 create_db: row.get::<_, bool>(1),
5454 create_role: row.get::<_, bool>(2),
5455 inherit: row.get::<_, bool>(3),
5456 })
5457 .collect();
5458
5459 raw.policies = client
5460 .query(queries::POLICIES_QUERY, &[])
5461 .await
5462 .map_err(|e| CliError::Other(format!("Failed to query policies: {e}")))?
5463 .into_iter()
5464 .map(|row| RawPolicyInfo {
5465 schema: row.get::<_, String>(0),
5466 table: row.get::<_, String>(1),
5467 name: row.get::<_, String>(2),
5468 as_clause: row.get::<_, String>(3),
5469 for_clause: row.get::<_, String>(4),
5470 to: row.get::<_, Vec<String>>(5),
5471 using: row.get::<_, Option<String>>(6),
5472 with_check: row.get::<_, Option<String>>(7),
5473 })
5474 .collect();
5475
5476 Ok(())
5477}
5478
5479#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
5481#[derive(Debug, Clone)]
5482struct RawSchemaInfo {
5483 name: String,
5484}
5485
5486#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
5492struct PostgresRawData {
5493 schemas: Vec<RawSchemaInfo>,
5494 tables: Vec<drizzle_migrations::postgres::introspect::RawTableInfo>,
5495 columns: Vec<drizzle_migrations::postgres::introspect::RawColumnInfo>,
5496 enums: Vec<drizzle_migrations::postgres::introspect::RawEnumInfo>,
5497 sequences: Vec<drizzle_migrations::postgres::introspect::RawSequenceInfo>,
5498 views: Vec<drizzle_migrations::postgres::introspect::RawViewInfo>,
5499 indexes: Vec<drizzle_migrations::postgres::introspect::RawIndexInfo>,
5500 foreign_keys: Vec<drizzle_migrations::postgres::introspect::RawForeignKeyInfo>,
5501 primary_keys: Vec<drizzle_migrations::postgres::introspect::RawPrimaryKeyInfo>,
5502 uniques: Vec<drizzle_migrations::postgres::introspect::RawUniqueInfo>,
5503 checks: Vec<drizzle_migrations::postgres::introspect::RawCheckInfo>,
5504 roles: Vec<drizzle_migrations::postgres::introspect::RawRoleInfo>,
5505 policies: Vec<drizzle_migrations::postgres::introspect::RawPolicyInfo>,
5506}
5507
5508#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
5512fn build_postgres_ddl(
5513 raw: PostgresRawData,
5514) -> drizzle_migrations::postgres::collection::PostgresDDL {
5515 use drizzle_migrations::postgres::ddl::Schema;
5516 use drizzle_migrations::postgres::introspect::{RawIntrospection, assemble_ddl};
5517
5518 assemble_ddl(RawIntrospection {
5519 schemas: raw
5520 .schemas
5521 .into_iter()
5522 .map(|schema| Schema::new(schema.name))
5523 .collect(),
5524 tables: raw.tables,
5525 columns: raw.columns,
5526 enums: raw.enums,
5527 sequences: raw.sequences,
5528 views: raw.views,
5529 indexes: raw.indexes,
5530 foreign_keys: raw.foreign_keys,
5531 primary_keys: raw.primary_keys,
5532 unique_constraints: raw.uniques,
5533 check_constraints: raw.checks,
5534 roles: raw.roles,
5535 policies: raw.policies,
5536 })
5537}
5538
5539#[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
5541fn finalize_postgres_introspection(
5542 ddl: &drizzle_migrations::postgres::collection::PostgresDDL,
5543 url: &str,
5544) -> IntrospectResult {
5545 use drizzle_migrations::postgres::codegen::{
5546 CodegenOptions, FieldCasing, generate_rust_schema,
5547 };
5548
5549 let options = CodegenOptions {
5550 module_doc: Some(format!(
5551 "Schema introspected from {}",
5552 crate::output::mask_url(url)
5553 )),
5554 include_schema: true,
5555 schema_name: "Schema".to_string(),
5556 use_pub: true,
5557 field_casing: FieldCasing::default(),
5558 };
5559 let generated = generate_rust_schema(ddl, &options);
5560
5561 let mut snap = drizzle_migrations::postgres::PostgresSnapshot::new();
5562 for entity in ddl.to_entities() {
5563 snap.add_entity(entity);
5564 }
5565
5566 IntrospectResult {
5567 schema_code: generated.code,
5568 table_count: ddl.tables.list().len(),
5569 index_count: ddl.indexes.list().len(),
5570 view_count: ddl.views.list().len(),
5571 warnings: generated.warnings,
5572 mysql_catalog_defaults: None,
5573 snapshot: Snapshot::Postgres(snap),
5574 snapshot_path: std::path::PathBuf::new(),
5575 }
5576}
5577
5578#[cfg(test)]
5579mod tests {
5580 use super::*;
5581
5582 #[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
5583 fn test_postgres_url() -> String {
5584 std::env::var("DATABASE_URL")
5585 .unwrap_or_else(|_| "postgres://postgres:postgres@localhost:5432/drizzle_test".into())
5586 }
5587
5588 #[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
5589 fn test_postgres_creds() -> crate::config::PostgresCreds {
5590 crate::config::PostgresCreds::Url(test_postgres_url().into_boxed_str())
5591 }
5592
5593 #[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
5594 fn unique_pg_name(prefix: &str) -> String {
5595 use std::time::{SystemTime, UNIX_EPOCH};
5596
5597 let nanos = SystemTime::now()
5598 .duration_since(UNIX_EPOCH)
5599 .expect("time went backwards")
5600 .as_nanos();
5601 format!("{}_{}_{}", prefix, std::process::id(), nanos)
5602 }
5603
5604 fn dropped(table: &str, column: Option<&str>) -> drizzle_migrations::DataLoss {
5605 drizzle_migrations::DataLoss {
5606 schema: None,
5607 table: table.to_string(),
5608 column: column.map(str::to_string),
5609 }
5610 }
5611
5612 #[test]
5615 fn push_confirms_only_drops_that_lose_data() {
5616 let drop_table = vec!["DROP TABLE \"scratch\";".to_string()];
5617 assert!(!push_requires_confirmation(
5618 Dialect::Postgresql,
5619 &drop_table,
5620 &[],
5621 &[(dropped("scratch", None), Some(0))],
5622 ));
5623 assert!(push_requires_confirmation(
5624 Dialect::Postgresql,
5625 &drop_table,
5626 &[],
5627 &[(dropped("scratch", None), Some(3))],
5628 ));
5629 assert!(push_requires_confirmation(
5631 Dialect::Postgresql,
5632 &drop_table,
5633 &[],
5634 &[(dropped("scratch", None), None)],
5635 ));
5636
5637 let drop_column = vec!["ALTER TABLE \"users\" DROP COLUMN \"nickname\";".to_string()];
5638 let nickname = dropped("users", Some("nickname"));
5639 assert!(!push_requires_confirmation(
5640 Dialect::Postgresql,
5641 &drop_column,
5642 &[],
5643 &[(nickname.clone(), Some(0))],
5644 ));
5645 assert!(push_requires_confirmation(
5646 Dialect::Postgresql,
5647 &drop_column,
5648 &[],
5649 &[(nickname, Some(1))],
5650 ));
5651
5652 let no_rows_lost = [
5653 "DROP INDEX \"users_email_idx\";",
5654 "DROP VIEW \"active_users\";",
5655 "DROP POLICY \"own_rows\" ON \"users\";",
5656 ]
5657 .map(str::to_string);
5658 assert!(!push_requires_confirmation(
5659 Dialect::Postgresql,
5660 &no_rows_lost,
5661 &[],
5662 &[],
5663 ));
5664 assert!(push_requires_confirmation(
5665 Dialect::Postgresql,
5666 &["TRUNCATE \"users\";".to_string()],
5667 &[],
5668 &[],
5669 ));
5670 }
5671
5672 #[test]
5675 fn push_confirms_recreated_columns() {
5676 let statements = [
5677 "ALTER TABLE \"users\" DROP COLUMN \"total\";",
5678 "ALTER TABLE \"users\" ADD COLUMN \"total\" integer GENERATED ALWAYS AS (a + b) STORED;",
5679 ]
5680 .map(str::to_string);
5681 assert!(push_requires_confirmation(
5682 Dialect::Postgresql,
5683 &statements,
5684 &[],
5685 &[],
5686 ));
5687 }
5688
5689 #[test]
5690 fn mysql_structural_warnings_require_push_confirmation() {
5691 let statements =
5692 vec!["ALTER TABLE `users` MODIFY COLUMN `email` varchar(32) NOT NULL;".to_string()];
5693 let warnings = vec![
5694 "changing the type of users.email can truncate or reject existing values".to_string(),
5695 ];
5696
5697 assert!(push_requires_confirmation(
5698 Dialect::Mysql,
5699 &statements,
5700 &warnings,
5701 &[],
5702 ));
5703 assert!(!push_requires_confirmation(
5704 Dialect::Mysql,
5705 &statements,
5706 &[],
5707 &[],
5708 ));
5709 }
5710
5711 #[cfg(feature = "rusqlite")]
5712 #[test]
5713 fn sqlite_migrations_run_both_when_created_at_collides() {
5714 use drizzle_migrations::{Migration, Migrations};
5718
5719 let dir = tempfile::tempdir().expect("tempdir");
5720 let db_path = dir.path().join("migrate.sqlite");
5721 let db_path_str = db_path.to_string_lossy().to_string();
5722
5723 let first_set = Migrations::new(
5724 vec![Migration::with_hash(
5725 "20230331141203_first",
5726 "hash_one",
5727 1_680_271_923_000,
5728 vec!["CREATE TABLE created_at_dedupe_a (id INTEGER PRIMARY KEY)".to_string()],
5729 )],
5730 drizzle_types::Dialect::SQLite,
5731 );
5732
5733 let first =
5734 run_sqlite_migrations(&first_set, &db_path_str, false).expect("first migrate succeeds");
5735 assert_eq!(first.applied_count, 1);
5736
5737 let second_set = Migrations::new(
5738 vec![
5739 Migration::with_hash(
5740 "20230331141203_first",
5741 "hash_one",
5742 1_680_271_923_000,
5743 vec!["CREATE TABLE created_at_dedupe_a (id INTEGER PRIMARY KEY)".to_string()],
5744 ),
5745 Migration::with_hash(
5746 "20230331141203_second",
5747 "hash_two",
5748 1_680_271_923_000,
5749 vec!["CREATE TABLE created_at_dedupe_b (id INTEGER PRIMARY KEY)".to_string()],
5750 ),
5751 ],
5752 drizzle_types::Dialect::SQLite,
5753 );
5754
5755 let second = run_sqlite_migrations(&second_set, &db_path_str, false)
5756 .expect("second migrate succeeds");
5757 assert_eq!(
5758 second.applied_count, 1,
5759 "only the second (newly introduced by name) migration should apply"
5760 );
5761
5762 let conn = rusqlite::Connection::open(&db_path).expect("open sqlite");
5763 let rows: i64 = conn
5764 .query_row("SELECT COUNT(*) FROM __drizzle_migrations", [], |row| {
5765 row.get(0)
5766 })
5767 .expect("count migrations rows");
5768 assert_eq!(rows, 2, "both migration records should be stored");
5769
5770 let table_b_exists: i64 = conn
5771 .query_row(
5772 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='created_at_dedupe_b'",
5773 [],
5774 |row| row.get(0),
5775 )
5776 .expect("query sqlite_master");
5777 assert_eq!(
5778 table_b_exists, 1,
5779 "the new-name migration must execute even though created_at collides"
5780 );
5781 }
5782
5783 #[cfg(feature = "rusqlite")]
5784 #[test]
5785 fn sqlite_cli_rebuild_preserves_cascade_children() {
5786 use drizzle_migrations::{Migration, Migrations};
5787
5788 let dir = tempfile::tempdir().expect("tempdir");
5789 let db_path = dir.path().join("rebuild.sqlite");
5790 let conn = rusqlite::Connection::open(&db_path).expect("open sqlite");
5791 conn.execute_batch(
5792 "PRAGMA foreign_keys=ON;
5793 CREATE TABLE rebuild_parent (id INTEGER PRIMARY KEY);
5794 CREATE TABLE rebuild_child (
5795 id INTEGER PRIMARY KEY,
5796 parent_id INTEGER NOT NULL REFERENCES rebuild_parent(id) ON DELETE CASCADE
5797 );
5798 INSERT INTO rebuild_parent VALUES (1);
5799 INSERT INTO rebuild_child VALUES (1, 1);",
5800 )
5801 .expect("seed related tables");
5802
5803 let migration = Migration::new(
5804 "20260825000000_rebuild",
5805 "PRAGMA foreign_keys=OFF;
5806 --> statement-breakpoint
5807 CREATE TABLE rebuild_parent_new (id INTEGER PRIMARY KEY) STRICT;
5808 --> statement-breakpoint
5809 INSERT INTO rebuild_parent_new SELECT * FROM rebuild_parent;
5810 --> statement-breakpoint
5811 DROP TABLE rebuild_parent;
5812 --> statement-breakpoint
5813 ALTER TABLE rebuild_parent_new RENAME TO rebuild_parent;
5814 --> statement-breakpoint
5815 PRAGMA foreign_keys=ON;",
5816 );
5817 let set = Migrations::new(vec![migration], drizzle_types::Dialect::SQLite);
5818
5819 run_sqlite_migrations_on_connection(&set, &conn, false).expect("run safe rebuild");
5820
5821 let children: i64 = conn
5822 .query_row("SELECT count(*) FROM rebuild_child", [], |row| row.get(0))
5823 .expect("count cascade children");
5824 assert_eq!(children, 1, "rebuild must not trigger ON DELETE CASCADE");
5825 let violations: i64 = conn
5826 .query_row("SELECT count(*) FROM pragma_foreign_key_check", [], |row| {
5827 row.get(0)
5828 })
5829 .expect("check rebuilt foreign keys");
5830 assert_eq!(violations, 0);
5831 let foreign_keys: i64 = conn
5832 .query_row("PRAGMA foreign_keys", [], |row| row.get(0))
5833 .expect("read restored foreign-key mode");
5834 assert_eq!(foreign_keys, 1);
5835 }
5836
5837 #[cfg(feature = "rusqlite")]
5838 #[test]
5839 fn sqlite_push_rebuild_preserves_cascade_children() {
5840 let conn = rusqlite::Connection::open_in_memory().expect("open sqlite");
5841 conn.execute_batch(
5842 "PRAGMA foreign_keys=ON;
5843 CREATE TABLE rebuild_parent (id INTEGER PRIMARY KEY);
5844 CREATE TABLE rebuild_child (
5845 id INTEGER PRIMARY KEY,
5846 parent_id INTEGER NOT NULL REFERENCES rebuild_parent(id) ON DELETE CASCADE
5847 );
5848 INSERT INTO rebuild_parent VALUES (1);
5849 INSERT INTO rebuild_child VALUES (1, 1);",
5850 )
5851 .expect("seed related tables");
5852 let migration = drizzle_migrations::Migration::new(
5853 "push_rebuild",
5854 "PRAGMA foreign_keys=OFF;
5855 --> statement-breakpoint
5856 CREATE TABLE rebuild_parent_new (id INTEGER PRIMARY KEY) STRICT;
5857 --> statement-breakpoint
5858 INSERT INTO rebuild_parent_new SELECT * FROM rebuild_parent;
5859 --> statement-breakpoint
5860 DROP TABLE rebuild_parent;
5861 --> statement-breakpoint
5862 ALTER TABLE rebuild_parent_new RENAME TO rebuild_parent;
5863 --> statement-breakpoint
5864 PRAGMA foreign_keys=ON;",
5865 );
5866
5867 execute_sqlite_statements_on_connection(&conn, migration.statements())
5868 .expect("apply push statements");
5869
5870 let children: i64 = conn
5871 .query_row("SELECT count(*) FROM rebuild_child", [], |row| row.get(0))
5872 .expect("count cascade children");
5873 assert_eq!(children, 1, "push rebuild must not trigger cascade");
5874 let foreign_keys: i64 = conn
5875 .query_row("PRAGMA foreign_keys", [], |row| row.get(0))
5876 .expect("read restored foreign-key mode");
5877 assert_eq!(foreign_keys, 1);
5878 }
5879
5880 #[cfg(feature = "rusqlite")]
5881 #[test]
5882 fn declined_destructive_push_is_an_abort_and_applies_nothing() {
5883 let dir = tempfile::tempdir().expect("tempdir");
5884 let db_path = dir.path().join("push.sqlite");
5885 rusqlite::Connection::open(&db_path)
5886 .expect("open sqlite")
5887 .execute_batch("CREATE TABLE keep_me (id INTEGER PRIMARY KEY);")
5888 .expect("seed table");
5889
5890 let connection = ResolvedConnection {
5891 dialect: Dialect::Sqlite,
5892 driver: Driver::Rusqlite,
5893 credentials: Credentials::Sqlite {
5894 path: db_path.to_string_lossy().into_owned().into_boxed_str(),
5895 },
5896 };
5897 let plan = PushPlan {
5898 sql_statements: vec!["DROP TABLE `keep_me`;".to_string()],
5899 warnings: Vec::new(),
5900 destructive: true,
5901 };
5902
5903 let error = apply_push_with_confirmation(&connection, &plan, false, || Ok(false))
5904 .expect_err("declining the prompt must not report success");
5905 assert!(matches!(error, CliError::Aborted(_)), "{error:?}");
5906 assert!(error.to_string().contains("aborted"), "{error}");
5907
5908 let kept: i64 = rusqlite::Connection::open(&db_path)
5909 .expect("reopen sqlite")
5910 .query_row(
5911 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='keep_me'",
5912 [],
5913 |row| row.get(0),
5914 )
5915 .expect("query sqlite_master");
5916 assert_eq!(kept, 1, "a declined push must not execute any statement");
5917 }
5918
5919 #[cfg(feature = "rusqlite")]
5920 #[test]
5921 fn run_migrations_creates_metadata_table_with_no_local_migrations() {
5922 let dir = tempfile::tempdir().expect("tempdir");
5923 let db_path = dir.path().join("empty.sqlite");
5924 let migrations_dir = dir.path().join("migrations");
5925 std::fs::create_dir_all(&migrations_dir).expect("create migrations dir");
5926
5927 let creds = crate::config::Credentials::Sqlite {
5928 path: db_path.to_string_lossy().to_string().into_boxed_str(),
5929 };
5930 let connection = ResolvedConnection {
5931 dialect: crate::config::Dialect::Sqlite,
5932 driver: Driver::Rusqlite,
5933 credentials: creds,
5934 };
5935
5936 let result = run_migrations(
5937 &connection,
5938 &migrations_dir,
5939 "__drizzle_migrations",
5940 "drizzle",
5941 false,
5942 )
5943 .expect("run migrations");
5944 assert_eq!(result.applied_count, 0);
5945
5946 let conn = rusqlite::Connection::open(&db_path).expect("open sqlite");
5947 let exists: i64 = conn
5948 .query_row(
5949 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='__drizzle_migrations'",
5950 [],
5951 |row| row.get(0),
5952 )
5953 .expect("check metadata table");
5954 assert_eq!(exists, 1, "migrations metadata table should be created");
5955 }
5956
5957 #[cfg(any(
5958 feature = "rusqlite",
5959 feature = "libsql",
5960 feature = "turso",
5961 feature = "postgres-sync",
5962 feature = "tokio-postgres",
5963 feature = "mysql-sync",
5964 feature = "mysql-async"
5965 ))]
5966 #[test]
5967 fn validate_init_metadata_matches_drizzle_orm_semantics() {
5968 use drizzle_migrations::{Migration, Migrations};
5969
5970 let empty_set = Migrations::empty(drizzle_types::Dialect::SQLite);
5971 validate_init_metadata(&[], &empty_set).expect("empty local migrations should be allowed");
5972
5973 let single = Migrations::new(
5974 vec![Migration::with_hash(
5975 "20230331141203_init",
5976 "hash_single",
5977 1_680_271_923_000,
5978 vec!["CREATE TABLE t(id INTEGER PRIMARY KEY)".to_string()],
5979 )],
5980 drizzle_types::Dialect::SQLite,
5981 );
5982 validate_init_metadata(&[], &single).expect("single local migration should be allowed");
5983
5984 let multiple = Migrations::new(
5985 vec![
5986 Migration::with_hash(
5987 "20230331141203_first",
5988 "hash_a",
5989 1_680_271_923_000,
5990 vec!["CREATE TABLE a(id INTEGER PRIMARY KEY)".to_string()],
5991 ),
5992 Migration::with_hash(
5993 "20230331150000_second",
5994 "hash_b",
5995 1_680_275_400_000,
5996 vec!["CREATE TABLE b(id INTEGER PRIMARY KEY)".to_string()],
5997 ),
5998 ],
5999 drizzle_types::Dialect::SQLite,
6000 );
6001
6002 let err =
6003 validate_init_metadata(&[], &multiple).expect_err("multiple local migrations rejected");
6004 assert_eq!(
6005 err.to_string(),
6006 "--init can't be used with existing migrations"
6007 );
6008
6009 let err = validate_init_metadata(&["20230331141203_init".to_string()], &single)
6010 .expect_err("existing db metadata should be rejected");
6011 assert_eq!(
6012 err.to_string(),
6013 "--init can't be used when database already has migrations set"
6014 );
6015 }
6016
6017 #[cfg(any(
6018 feature = "rusqlite",
6019 feature = "libsql",
6020 feature = "turso",
6021 feature = "postgres-sync",
6022 feature = "tokio-postgres",
6023 feature = "mysql-sync",
6024 feature = "mysql-async",
6025 feature = "d1-http",
6026 ))]
6027 #[test]
6028 fn integrity_findings_detect_hash_drift() {
6029 use drizzle_migrations::{Migration, Migrations};
6030
6031 let set = Migrations::new(
6032 vec![Migration::with_hash(
6033 "20230331141203_verify",
6034 "local_hash",
6035 1_680_271_923_000,
6036 vec!["CREATE TABLE t(id INTEGER PRIMARY KEY)".to_string()],
6037 )],
6038 drizzle_types::Dialect::SQLite,
6039 );
6040
6041 let applied = vec![AppliedMigrationRecord {
6042 hash: "db_hash".to_string(),
6043 name: "20230331141203_verify".to_string(),
6044 dirty: false,
6045 }];
6046
6047 let findings =
6048 collect_integrity_findings(&set, &applied).expect("local set is well-formed");
6049 assert_eq!(findings.len(), 1, "expected one finding: {findings:?}");
6050 assert!(
6051 findings[0].contains("20230331141203_verify") && findings[0].contains("drifted"),
6052 "unexpected finding: {}",
6053 findings[0]
6054 );
6055 }
6056
6057 #[cfg(any(
6058 feature = "rusqlite",
6059 feature = "libsql",
6060 feature = "turso",
6061 feature = "postgres-sync",
6062 feature = "tokio-postgres",
6063 feature = "mysql-sync",
6064 feature = "mysql-async",
6065 feature = "d1-http",
6066 ))]
6067 #[test]
6068 fn integrity_findings_report_dirty_and_missing_local_rows() {
6069 use drizzle_migrations::{Migration, Migrations};
6070
6071 let set = Migrations::new(
6072 vec![Migration::with_hash(
6073 "20230331141203_first",
6074 "hash_a",
6075 1_680_271_923_000,
6076 vec!["CREATE TABLE a(id INTEGER PRIMARY KEY)".to_string()],
6077 )],
6078 drizzle_types::Dialect::SQLite,
6079 );
6080
6081 let applied = vec![
6082 AppliedMigrationRecord {
6083 hash: "hash_a".to_string(),
6084 name: "20230331141203_first".to_string(),
6085 dirty: true,
6086 },
6087 AppliedMigrationRecord {
6088 hash: "hash_gone".to_string(),
6089 name: "20230401000000_deleted_locally".to_string(),
6090 dirty: false,
6091 },
6092 ];
6093
6094 let findings =
6095 collect_integrity_findings(&set, &applied).expect("local set is well-formed");
6096 assert_eq!(findings.len(), 2, "expected two findings: {findings:?}");
6097 assert!(
6098 findings[0].contains("interrupted mid-apply"),
6099 "unexpected finding: {}",
6100 findings[0]
6101 );
6102 assert!(
6103 findings[1].contains("no local migration"),
6104 "unexpected finding: {}",
6105 findings[1]
6106 );
6107 }
6108
6109 #[cfg(any(
6110 feature = "rusqlite",
6111 feature = "libsql",
6112 feature = "turso",
6113 feature = "postgres-sync",
6114 feature = "tokio-postgres",
6115 feature = "mysql-sync",
6116 feature = "mysql-async",
6117 feature = "d1-http",
6118 ))]
6119 #[test]
6120 fn mysql_integrity_findings_do_not_recommend_automatic_repair() {
6121 use drizzle_migrations::{Migration, Migrations};
6122
6123 let set = Migrations::new(
6124 vec![Migration::with_hash(
6125 "20230331141203_first",
6126 "hash_a",
6127 1_680_271_923_000,
6128 vec!["CREATE TABLE a(id INTEGER PRIMARY KEY)".to_string()],
6129 )],
6130 drizzle_types::Dialect::MySQL,
6131 );
6132 let findings = collect_integrity_findings(
6133 &set,
6134 &[AppliedMigrationRecord {
6135 hash: "hash_a".to_string(),
6136 name: "20230331141203_first".to_string(),
6137 dirty: true,
6138 }],
6139 )
6140 .expect("local set is well-formed");
6141
6142 assert!(findings[0].contains("resolve it manually"), "{findings:?}");
6143 assert!(!findings[0].contains("--repair"), "{findings:?}");
6144 }
6145
6146 #[cfg(any(
6147 feature = "rusqlite",
6148 feature = "libsql",
6149 feature = "turso",
6150 feature = "postgres-sync",
6151 feature = "tokio-postgres",
6152 feature = "mysql-sync",
6153 feature = "mysql-async",
6154 feature = "d1-http",
6155 ))]
6156 #[test]
6157 fn dirty_rows_are_not_counted_as_applied_in_plan() {
6158 use drizzle_migrations::{Migration, Migrations};
6159
6160 let set = Migrations::new(
6161 vec![Migration::with_hash(
6162 "20230331141203_first",
6163 "hash_a",
6164 1_680_271_923_000,
6165 vec!["CREATE TABLE a(id INTEGER PRIMARY KEY)".to_string()],
6166 )],
6167 drizzle_types::Dialect::SQLite,
6168 );
6169
6170 let applied = vec![AppliedMigrationRecord {
6171 hash: "hash_a".to_string(),
6172 name: "20230331141203_first".to_string(),
6173 dirty: true,
6174 }];
6175
6176 let plan = build_migration_plan(&set, &applied).expect("build migration plan");
6177 assert_eq!(plan.applied_count, 0);
6178 assert_eq!(plan.pending_count, 1, "interrupted migration is unfinished");
6179 assert_eq!(plan.findings.len(), 1);
6180 }
6181
6182 #[cfg(feature = "rusqlite")]
6185 #[test]
6186 fn sqlite_plan_creates_neither_the_database_nor_the_tracking_table() {
6187 use drizzle_migrations::{Migration, Migrations};
6188
6189 let set = Migrations::new(
6190 vec![Migration::with_hash(
6191 "20230331141203_first",
6192 "hash_a",
6193 1_680_271_923_000,
6194 vec!["CREATE TABLE a(id INTEGER PRIMARY KEY)".to_string()],
6195 )],
6196 drizzle_types::Dialect::SQLite,
6197 );
6198 let dir = tempfile::tempdir().expect("tempdir");
6199
6200 let missing = dir.path().join("missing.db");
6201 let plan = inspect_sqlite_migrations(&set, missing.to_str().expect("utf-8 path"))
6202 .expect("plan a missing database");
6203 assert_eq!(plan.pending_count, 1);
6204 assert!(!missing.exists(), "planning created the database file");
6205
6206 let existing = dir.path().join("existing.db");
6207 rusqlite::Connection::open(&existing)
6208 .and_then(|conn| conn.execute("CREATE TABLE users(id INTEGER PRIMARY KEY)", []))
6209 .expect("seed database");
6210 let plan = inspect_sqlite_migrations(&set, existing.to_str().expect("utf-8 path"))
6211 .expect("plan an existing database");
6212 assert_eq!(plan.pending_count, 1);
6213 let tracking_tables: i64 = rusqlite::Connection::open(&existing)
6214 .and_then(|conn| {
6215 conn.query_row(
6216 "SELECT count(*) FROM sqlite_master WHERE name = ?1",
6217 [set.table_name()],
6218 |row| row.get(0),
6219 )
6220 })
6221 .expect("inspect database");
6222 assert_eq!(tracking_tables, 0, "planning created the tracking table");
6223 }
6224
6225 #[cfg(any(
6226 feature = "rusqlite",
6227 feature = "libsql",
6228 feature = "turso",
6229 feature = "postgres-sync",
6230 feature = "tokio-postgres",
6231 feature = "mysql-sync",
6232 feature = "mysql-async",
6233 feature = "d1-http",
6234 ))]
6235 #[test]
6236 fn build_migration_plan_counts_pending_statements() {
6237 use drizzle_migrations::{Migration, Migrations};
6238
6239 let set = Migrations::new(
6240 vec![
6241 Migration::with_hash(
6242 "20230331141203_first",
6243 "hash_a",
6244 1_680_271_923_000,
6245 vec![
6246 "CREATE TABLE a(id INTEGER PRIMARY KEY)".to_string(),
6247 "CREATE INDEX a_id_idx ON a(id)".to_string(),
6248 ],
6249 ),
6250 Migration::with_hash(
6251 "20230331150000_second",
6252 "hash_b",
6253 1_680_275_400_000,
6254 vec!["CREATE TABLE b(id INTEGER PRIMARY KEY)".to_string()],
6255 ),
6256 ],
6257 drizzle_types::Dialect::SQLite,
6258 );
6259
6260 let applied = vec![AppliedMigrationRecord {
6261 hash: "hash_a".to_string(),
6262 name: "20230331141203_first".to_string(),
6263 dirty: false,
6264 }];
6265
6266 let plan = build_migration_plan(&set, &applied).expect("build migration plan");
6267 assert_eq!(plan.applied_count, 1);
6268 assert_eq!(plan.pending_count, 1);
6269 assert_eq!(plan.pending_statements, 1);
6270 assert_eq!(plan.pending_migrations, vec!["20230331150000_second"]);
6271 assert!(plan.findings.is_empty());
6272 }
6273
6274 #[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
6275 #[test]
6276 fn postgres_concurrent_index_detection_is_case_insensitive() {
6277 assert!(is_postgres_concurrent_index_statement(
6278 "CREATE INDEX CONCURRENTLY users_email_idx ON users (email);"
6279 ));
6280 assert!(is_postgres_concurrent_index_statement(
6281 "CREATE UNIQUE INDEX CONCURRENTLY users_email_idx ON users (email);"
6282 ));
6283 assert!(is_postgres_concurrent_index_statement(
6284 "drop index concurrently users_email_idx;"
6285 ));
6286 assert!(!is_postgres_concurrent_index_statement(
6287 "CREATE INDEX users_email_idx ON users (email);"
6288 ));
6289 }
6290
6291 #[cfg(any(feature = "postgres-sync", feature = "tokio-postgres"))]
6292 #[test]
6293 fn postgres_concurrent_index_detection_over_statement_list() {
6294 let with_concurrent = vec![
6295 "CREATE TABLE users(id integer);".to_string(),
6296 "CREATE INDEX CONCURRENTLY users_email_idx ON users (id);".to_string(),
6297 ];
6298 let without_concurrent = vec![
6299 "CREATE TABLE users(id integer);".to_string(),
6300 "CREATE INDEX users_email_idx ON users (id);".to_string(),
6301 ];
6302
6303 assert!(has_postgres_concurrent_index(&with_concurrent));
6304 assert!(!has_postgres_concurrent_index(&without_concurrent));
6305 }
6306
6307 #[test]
6308 fn generate_push_sql_includes_concurrent_postgres_index() {
6309 use drizzle_migrations::parser::SchemaParser;
6310 use drizzle_types::Dialect;
6311
6312 let previous = r#"
6313#[PostgresTable]
6314pub struct Users {
6315 #[column(primary)]
6316 pub id: i32,
6317 pub email: String,
6318}
6319"#;
6320
6321 let current = r#"
6322#[PostgresTable]
6323pub struct Users {
6324 #[column(primary)]
6325 pub id: i32,
6326 pub email: String,
6327}
6328
6329#[PostgresIndex(concurrent)]
6330pub struct UsersEmailIdx(Users::email);
6331"#;
6332
6333 let prev =
6334 Snapshot::from_parse_result(&SchemaParser::parse(previous), Dialect::PostgreSQL, None);
6335 let curr =
6336 Snapshot::from_parse_result(&SchemaParser::parse(current), Dialect::PostgreSQL, None);
6337
6338 let plan = generate_push_sql(&prev, &curr, false, &drizzle_migrations::DiffOptions::new())
6339 .expect("push sql generation");
6340 let (sql, warnings) = (plan.statements, plan.warnings);
6341 assert!(warnings.is_empty());
6342 assert_eq!(sql.len(), 1);
6343 assert_eq!(
6344 sql[0],
6345 "CREATE INDEX CONCURRENTLY \"users_email_idx\" ON \"users\"(\"email\");"
6346 );
6347 }
6348
6349 #[test]
6350 fn generate_push_sql_uses_live_mysql_catalog_defaults() {
6351 use drizzle_migrations::mysql::MySQLCatalogDefaults;
6352 use drizzle_migrations::parser::SchemaParser;
6353 use drizzle_types::Dialect;
6354
6355 let introspected = r#"
6356#[MySQLTable(NAME = "users")]
6357pub struct Users {
6358 #[column(PRIMARY)]
6359 pub id: i32,
6360}
6361"#;
6362 let desired = r#"
6363#[MySQLTable(NAME = "users", ENGINE = "InnoDB")]
6364pub struct Users {
6365 #[column(PRIMARY)]
6366 pub id: i32,
6367}
6368"#;
6369 let current =
6370 Snapshot::from_parse_result(&SchemaParser::parse(introspected), Dialect::MySQL, None);
6371 let desired =
6372 Snapshot::from_parse_result(&SchemaParser::parse(desired), Dialect::MySQL, None);
6373 let options = drizzle_migrations::DiffOptions::new()
6374 .mysql_catalog_defaults(MySQLCatalogDefaults::new().engine("InnoDB"));
6375
6376 let plan = generate_push_sql(¤t, &desired, false, &options)
6377 .expect("push SQL with live MySQL defaults");
6378 let (sql, warnings) = (plan.statements, plan.warnings);
6379 assert!(sql.is_empty(), "inherited defaults must not churn: {sql:?}");
6380 assert!(warnings.is_empty());
6381 }
6382
6383 #[test]
6384 fn filter_patterns_support_negation_globs() {
6385 let raw = vec![
6386 "users_*".to_string(),
6387 "!users_4".to_string(),
6388 "!ad*".to_string(),
6389 ];
6390 let patterns = compile_patterns(Some(&raw)).expect("compile patterns");
6391
6392 assert!(matches_patterns("users_1", patterns.as_deref()));
6393 assert!(!matches_patterns("users_4", patterns.as_deref()));
6394 assert!(!matches_patterns("admin", patterns.as_deref()));
6395 assert!(!matches_patterns("audit", patterns.as_deref()));
6396 }
6397
6398 #[test]
6399 fn postgres_table_filter_matches_table_name_only() {
6400 use drizzle_migrations::parser::SchemaParser;
6401 use drizzle_migrations::schema::Snapshot;
6402 use drizzle_types::Dialect as BaseDialect;
6403
6404 let code = r#"
6405#[PostgresTable(schema = "admin")]
6406pub struct AuditLog {
6407 #[column(primary)]
6408 pub id: i32,
6409}
6410
6411#[PostgresTable(schema = "public")]
6412pub struct Users {
6413 #[column(primary)]
6414 pub id: i32,
6415}
6416"#;
6417
6418 let parsed = SchemaParser::parse(code);
6419 let mut snapshot = Snapshot::from_parse_result(&parsed, BaseDialect::PostgreSQL, None);
6420 let filters = SnapshotFilters {
6421 tables: Some(vec!["admin.*".to_string()]),
6422 schemas: None,
6423 extensions: None,
6424 roles: None,
6425 };
6426
6427 apply_snapshot_filters(&mut snapshot, crate::config::Dialect::Postgresql, &filters)
6428 .expect("apply filters");
6429
6430 let remaining_tables = match snapshot {
6431 Snapshot::Postgres(s) => s
6432 .ddl
6433 .iter()
6434 .filter_map(|e| {
6435 if let drizzle_types::postgres::ddl::PostgresEntity::Table(t) = e {
6436 Some((t.schema.to_string(), t.name.to_string()))
6437 } else {
6438 None
6439 }
6440 })
6441 .collect::<Vec<_>>(),
6442 _ => panic!("expected postgres snapshot"),
6443 };
6444
6445 assert!(remaining_tables.is_empty());
6446 }
6447
6448 #[test]
6449 fn postgres_schema_and_table_filters_intersect() {
6450 use drizzle_migrations::parser::SchemaParser;
6451 use drizzle_migrations::schema::Snapshot;
6452 use drizzle_types::Dialect as BaseDialect;
6453
6454 let code = r#"
6455#[PostgresTable(schema = "dev")]
6456pub struct UsersDev {
6457 #[column(primary)]
6458 pub id: i32,
6459}
6460
6461#[PostgresTable(schema = "public")]
6462pub struct UsersPublic {
6463 #[column(primary)]
6464 pub id: i32,
6465}
6466"#;
6467
6468 let parsed = SchemaParser::parse(code);
6469 let mut snapshot = Snapshot::from_parse_result(&parsed, BaseDialect::PostgreSQL, None);
6470 let filters = SnapshotFilters {
6471 tables: Some(vec!["users_*".to_string()]),
6472 schemas: Some(vec!["!dev".to_string()]),
6473 extensions: None,
6474 roles: None,
6475 };
6476
6477 apply_snapshot_filters(&mut snapshot, crate::config::Dialect::Postgresql, &filters)
6478 .expect("apply filters");
6479
6480 let remaining_tables = match snapshot {
6481 Snapshot::Postgres(s) => s
6482 .ddl
6483 .iter()
6484 .filter_map(|e| {
6485 if let drizzle_types::postgres::ddl::PostgresEntity::Table(t) = e {
6486 Some((t.schema.to_string(), t.name.to_string()))
6487 } else {
6488 None
6489 }
6490 })
6491 .collect::<Vec<_>>(),
6492 _ => panic!("expected postgres snapshot"),
6493 };
6494
6495 assert_eq!(
6496 remaining_tables,
6497 vec![("public".to_string(), "users_public".to_string())]
6498 );
6499 }
6500
6501 #[test]
6502 fn postgres_extensions_filter_excludes_postgis_internal_objects() {
6503 use drizzle_migrations::parser::SchemaParser;
6504 use drizzle_migrations::schema::Snapshot;
6505 use drizzle_types::Dialect as BaseDialect;
6506
6507 let code = r#"
6508#[PostgresTable(schema = "topology")]
6509pub struct TopologyLayer {
6510 #[column(primary)]
6511 pub id: i32,
6512}
6513
6514#[PostgresTable]
6515pub struct SpatialRefSys {
6516 #[column(primary)]
6517 pub id: i32,
6518}
6519
6520#[PostgresTable]
6521pub struct Users {
6522 #[column(primary)]
6523 pub id: i32,
6524}
6525"#;
6526
6527 let parsed = SchemaParser::parse(code);
6528 let mut snapshot = Snapshot::from_parse_result(&parsed, BaseDialect::PostgreSQL, None);
6529 let filters = SnapshotFilters {
6530 tables: None,
6531 schemas: None,
6532 extensions: Some(vec![Extension::Postgis]),
6533 roles: None,
6534 };
6535
6536 apply_snapshot_filters(&mut snapshot, crate::config::Dialect::Postgresql, &filters)
6537 .expect("apply filters");
6538
6539 let remaining_tables = match snapshot {
6540 Snapshot::Postgres(s) => s
6541 .ddl
6542 .iter()
6543 .filter_map(|e| {
6544 if let drizzle_types::postgres::ddl::PostgresEntity::Table(t) = e {
6545 Some((t.schema.to_string(), t.name.to_string()))
6546 } else {
6547 None
6548 }
6549 })
6550 .collect::<Vec<_>>(),
6551 _ => panic!("expected postgres snapshot"),
6552 };
6553
6554 assert_eq!(
6555 remaining_tables,
6556 vec![("public".to_string(), "users".to_string())]
6557 );
6558 }
6559
6560 #[test]
6561 fn format_migration_sql_respects_breakpoints_flag() {
6562 let sql = vec![
6563 "CREATE TABLE users(id integer);".to_string(),
6564 "CREATE INDEX users_id_idx ON users(id);".to_string(),
6565 ];
6566
6567 let with_breakpoints = format_migration_sql(&sql, true);
6568 assert_eq!(
6569 with_breakpoints,
6570 "CREATE TABLE users(id integer);\n--> statement-breakpoint\nCREATE INDEX users_id_idx ON users(id);"
6571 );
6572
6573 let without_breakpoints = format_migration_sql(&sql, false);
6574 assert_eq!(
6575 without_breakpoints,
6576 "CREATE TABLE users(id integer);\n\nCREATE INDEX users_id_idx ON users(id);"
6577 );
6578
6579 let empty = format_migration_sql(&[], false);
6580 assert_eq!(empty, "-- No tables to create (empty database)\n");
6581 }
6582
6583 #[test]
6584 fn regenerate_sqlite_schema_applies_introspect_casing() {
6585 use crate::config::{Casing, IntrospectCasing};
6586 use drizzle_migrations::parser::SchemaParser;
6587 use drizzle_types::Dialect as BaseDialect;
6588
6589 let code = r#"
6590#[SQLiteTable]
6591pub struct AuditLogs {
6592 #[column(primary)]
6593 pub id: i64,
6594 pub user_name: String,
6595}
6596"#;
6597
6598 let parsed = SchemaParser::parse(code);
6599 let snapshot =
6600 Snapshot::from_parse_result(&parsed, BaseDialect::SQLite, Some(Casing::SnakeCase));
6601
6602 let mut camel = IntrospectResult {
6603 schema_code: String::new(),
6604 table_count: 0,
6605 index_count: 0,
6606 view_count: 0,
6607 warnings: Vec::new(),
6608 mysql_catalog_defaults: None,
6609 snapshot: snapshot.clone(),
6610 snapshot_path: std::path::PathBuf::new(),
6611 };
6612 regenerate_schema_from_snapshot(
6613 &mut camel,
6614 crate::config::Dialect::Sqlite,
6615 Some(IntrospectCasing::Camel),
6616 );
6617
6618 assert_eq!(
6619 camel.schema_code,
6620 "\
6621//! Auto-generated SQLite schema from introspection
6622//!
6623//! Schema introspected from filtered database objects
6624
6625use drizzle::sqlite::prelude::*;
6626
6627#[SQLiteTable]
6628pub struct AuditLogs {
6629 #[column(primary)]
6630 pub id: i64,
6631 pub userName: String,
6632}
6633
6634#[derive(SQLiteSchema)]
6635pub struct Schema {
6636 pub auditLogs: AuditLogs,
6637}
6638"
6639 );
6640
6641 let mut preserve = IntrospectResult {
6642 schema_code: String::new(),
6643 table_count: 0,
6644 index_count: 0,
6645 view_count: 0,
6646 warnings: Vec::new(),
6647 mysql_catalog_defaults: None,
6648 snapshot,
6649 snapshot_path: std::path::PathBuf::new(),
6650 };
6651 regenerate_schema_from_snapshot(
6652 &mut preserve,
6653 crate::config::Dialect::Sqlite,
6654 Some(IntrospectCasing::Preserve),
6655 );
6656
6657 assert_eq!(
6658 preserve.schema_code,
6659 "\
6660//! Auto-generated SQLite schema from introspection
6661//!
6662//! Schema introspected from filtered database objects
6663
6664use drizzle::sqlite::prelude::*;
6665
6666#[SQLiteTable]
6667pub struct AuditLogs {
6668 #[column(primary)]
6669 pub id: i64,
6670 pub user_name: String,
6671}
6672
6673#[derive(SQLiteSchema)]
6674pub struct Schema {
6675 pub audit_logs: AuditLogs,
6676}
6677"
6678 );
6679 }
6680
6681 #[test]
6682 fn regenerate_postgres_schema_applies_introspect_casing() {
6683 use crate::config::{Casing, IntrospectCasing};
6684 use drizzle_migrations::parser::SchemaParser;
6685 use drizzle_types::Dialect as BaseDialect;
6686
6687 let code = r#"
6688#[PostgresTable]
6689pub struct AuditLogs {
6690 #[column(primary)]
6691 pub id: i32,
6692 pub user_name: String,
6693}
6694"#;
6695
6696 let parsed = SchemaParser::parse(code);
6697 let snapshot =
6698 Snapshot::from_parse_result(&parsed, BaseDialect::PostgreSQL, Some(Casing::SnakeCase));
6699
6700 let mut camel = IntrospectResult {
6701 schema_code: String::new(),
6702 table_count: 0,
6703 index_count: 0,
6704 view_count: 0,
6705 warnings: Vec::new(),
6706 mysql_catalog_defaults: None,
6707 snapshot: snapshot.clone(),
6708 snapshot_path: std::path::PathBuf::new(),
6709 };
6710 regenerate_schema_from_snapshot(
6711 &mut camel,
6712 crate::config::Dialect::Postgresql,
6713 Some(IntrospectCasing::Camel),
6714 );
6715
6716 assert_eq!(
6717 camel.schema_code,
6718 "\
6719//! Auto-generated PostgreSQL schema from introspection
6720//!
6721//! Schema introspected from filtered database objects
6722
6723use drizzle::postgres::prelude::*;
6724
6725#[PostgresTable]
6726pub struct AuditLogs {
6727 #[column(primary)]
6728 pub id: i32,
6729 pub userName: String,
6730}
6731
6732#[derive(PostgresSchema)]
6733pub struct Schema {
6734 pub auditLogs: AuditLogs,
6735}
6736"
6737 );
6738
6739 let mut preserve = IntrospectResult {
6740 schema_code: String::new(),
6741 table_count: 0,
6742 index_count: 0,
6743 view_count: 0,
6744 warnings: Vec::new(),
6745 mysql_catalog_defaults: None,
6746 snapshot,
6747 snapshot_path: std::path::PathBuf::new(),
6748 };
6749 regenerate_schema_from_snapshot(
6750 &mut preserve,
6751 crate::config::Dialect::Postgresql,
6752 Some(IntrospectCasing::Preserve),
6753 );
6754
6755 assert_eq!(
6756 preserve.schema_code,
6757 "\
6758//! Auto-generated PostgreSQL schema from introspection
6759//!
6760//! Schema introspected from filtered database objects
6761
6762use drizzle::postgres::prelude::*;
6763
6764#[PostgresTable]
6765pub struct AuditLogs {
6766 #[column(primary)]
6767 pub id: i32,
6768 pub user_name: String,
6769}
6770
6771#[derive(PostgresSchema)]
6772pub struct Schema {
6773 pub audit_logs: AuditLogs,
6774}
6775"
6776 );
6777 }
6778
6779 #[cfg(feature = "postgres-sync")]
6780 #[test]
6781 fn postgres_sync_migrate_applies_concurrent_index_without_transaction() {
6782 use drizzle_migrations::{Migration, Migrations};
6783 use drizzle_types::Dialect;
6784
6785 let creds = test_postgres_creds();
6786 let mut setup_client = match connect_postgres_sync(&creds) {
6787 Ok(c) => c,
6788 Err(e) => {
6789 eprintln!(
6790 "Skipping postgres_sync_migrate_applies_concurrent_index_without_transaction: {}",
6791 e
6792 );
6793 return;
6794 }
6795 };
6796
6797 let table = unique_pg_name("cli_sync_users");
6798 let index = format!("{}_email_idx", table);
6799 let migration_schema = unique_pg_name("cli_sync_mig");
6800
6801 setup_client
6802 .batch_execute(&format!(
6803 "DROP TABLE IF EXISTS \"{table}\" CASCADE; \
6804 CREATE TABLE \"{table}\" (id integer, email text NOT NULL); \
6805 INSERT INTO \"{table}\" (id, email) VALUES (1, 'a@example.com');"
6806 ))
6807 .expect("setup table for concurrent index test");
6808 drop(setup_client);
6809
6810 let migration_tag = format!("20260212000000_{table}");
6811 let migration_sql =
6812 format!("CREATE INDEX CONCURRENTLY \"{index}\" ON \"{table}\" (\"email\");");
6813 let set = Migrations::with_tracking(
6814 vec![Migration::new(&migration_tag, &migration_sql)],
6815 Dialect::PostgreSQL,
6816 drizzle_migrations::Tracking::POSTGRES.schema(migration_schema.clone()),
6817 );
6818
6819 let result = run_postgres_sync_migrations(&set, &creds, false)
6820 .expect("sync migration with concurrent index should succeed");
6821 assert_eq!(result.applied_count, 1);
6822
6823 let mut verify_client = connect_postgres_sync(&creds).expect("reconnect for verification");
6824 let exists: i64 = verify_client
6825 .query_one(
6826 "SELECT COUNT(*)::bigint FROM pg_indexes \
6827 WHERE schemaname = 'public' AND tablename = $1 AND indexname = $2",
6828 &[&table, &index],
6829 )
6830 .expect("query pg_indexes")
6831 .get(0);
6832 assert_eq!(exists, 1, "concurrent index was not created");
6833
6834 let _ = verify_client.batch_execute(&format!(
6835 "DROP TABLE IF EXISTS \"{table}\" CASCADE; \
6836 DROP SCHEMA IF EXISTS \"{migration_schema}\" CASCADE;"
6837 ));
6838 }
6839
6840 #[cfg(feature = "postgres-sync")]
6841 #[test]
6842 fn postgres_sync_migrate_upgrades_legacy_tracking_table_and_applies_pending() {
6843 use drizzle_migrations::{Migration, Migrations};
6844 use drizzle_types::Dialect;
6845
6846 let creds = test_postgres_creds();
6847 let mut client = connect_postgres_sync(&creds)
6848 .expect("connect postgres for legacy tracking upgrade test");
6849
6850 let applied_table = unique_pg_name("cli_sync_applied");
6851 let pending_table = unique_pg_name("cli_sync_pending");
6852 let migration_schema = unique_pg_name("cli_sync_tracking");
6853
6854 client
6855 .batch_execute(&format!(
6856 "DROP TABLE IF EXISTS \"{applied_table}\" CASCADE; \
6857 DROP TABLE IF EXISTS \"{pending_table}\" CASCADE; \
6858 DROP SCHEMA IF EXISTS \"{migration_schema}\" CASCADE; \
6859 CREATE SCHEMA \"{migration_schema}\"; \
6860 CREATE TABLE \"{migration_schema}\".\"__drizzle_migrations\" (id SERIAL PRIMARY KEY, hash TEXT NOT NULL, created_at BIGINT); \
6861 CREATE TABLE \"{applied_table}\" (id integer primary key);"
6862 ))
6863 .expect("setup legacy postgres tracking metadata");
6864
6865 let first = Migration::new(
6866 &format!("20230331141203_{applied_table}"),
6867 &format!("CREATE TABLE \"{applied_table}\" (id integer primary key);"),
6868 );
6869 let second = Migration::new(
6870 &format!("20230331141204_{pending_table}"),
6871 &format!("CREATE TABLE \"{pending_table}\" (id integer primary key);"),
6872 );
6873 let set = Migrations::with_tracking(
6874 vec![first.clone(), second.clone()],
6875 Dialect::PostgreSQL,
6876 drizzle_migrations::Tracking::POSTGRES.schema(migration_schema.clone()),
6877 );
6878
6879 client
6880 .execute(
6881 &format!(
6882 "INSERT INTO \"{migration_schema}\".\"__drizzle_migrations\" (hash, created_at) VALUES ($1, $2)"
6883 ),
6884 &[&first.hash(), &first.created_at()],
6885 )
6886 .expect("insert legacy applied migration row");
6887 drop(client);
6888
6889 let result = run_postgres_sync_migrations(&set, &creds, false)
6890 .expect("sync migration upgrade should succeed");
6891 assert_eq!(result.applied_count, 1);
6892 assert_eq!(result.applied_migrations, vec![second.hash().to_string()]);
6893
6894 let mut verify_client = connect_postgres_sync(&creds).expect("reconnect for verification");
6895 let columns: Vec<String> = verify_client
6896 .query(
6897 "SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = '__drizzle_migrations' ORDER BY ordinal_position",
6898 &[&migration_schema],
6899 )
6900 .expect("query upgraded tracking columns")
6901 .into_iter()
6902 .map(|row| row.get(0))
6903 .collect();
6904 assert_eq!(
6905 columns,
6906 vec!["id", "hash", "created_at", "name", "applied_at"]
6907 );
6908
6909 let rows: Vec<(String, i64, String, Option<String>)> = verify_client
6910 .query(
6911 &format!(
6912 "SELECT hash, created_at, name, applied_at::text FROM \"{migration_schema}\".\"__drizzle_migrations\" ORDER BY id ASC"
6913 ),
6914 &[],
6915 )
6916 .expect("query upgraded metadata rows")
6917 .into_iter()
6918 .map(|row| (row.get(0), row.get(1), row.get(2), row.get(3)))
6919 .collect();
6920 assert_eq!(rows.len(), 2);
6921 assert_eq!(rows[0].0, first.hash());
6922 assert_eq!(rows[0].1, first.created_at());
6923 assert_eq!(rows[0].2, first.name());
6924 assert!(
6925 rows[0].3.is_some(),
6926 "legacy rows get applied_at backfilled so they cannot read as interrupted"
6927 );
6928 assert_eq!(rows[1].0, second.hash());
6929 assert_eq!(rows[1].1, second.created_at());
6930 assert_eq!(rows[1].2, second.name());
6931 assert!(rows[1].3.is_some());
6932
6933 let pending_exists: i64 = verify_client
6934 .query_one(
6935 "SELECT COUNT(*)::bigint FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1",
6936 &[&pending_table],
6937 )
6938 .expect("query pending table")
6939 .get(0);
6940 assert_eq!(pending_exists, 1);
6941
6942 let _ = verify_client.batch_execute(&format!(
6943 "DROP TABLE IF EXISTS \"{applied_table}\" CASCADE; \
6944 DROP TABLE IF EXISTS \"{pending_table}\" CASCADE; \
6945 DROP SCHEMA IF EXISTS \"{migration_schema}\" CASCADE;"
6946 ));
6947 }
6948
6949 #[cfg(feature = "postgres-sync")]
6953 #[test]
6954 fn postgres_sync_migrate_finishes_half_upgraded_tracking_table() {
6955 use drizzle_migrations::{Migration, Migrations};
6956 use drizzle_types::Dialect;
6957
6958 let creds = test_postgres_creds();
6959 let mut client = connect_postgres_sync(&creds).expect("connect postgres");
6960 let migration_schema = unique_pg_name("cli_sync_half_upgraded");
6961 client
6962 .batch_execute(&format!(
6963 "DROP SCHEMA IF EXISTS \"{migration_schema}\" CASCADE; \
6964 CREATE SCHEMA \"{migration_schema}\"; \
6965 CREATE TABLE \"{migration_schema}\".\"__drizzle_migrations\" \
6966 (id SERIAL PRIMARY KEY, hash TEXT NOT NULL, created_at BIGINT, name TEXT); \
6967 INSERT INTO \"{migration_schema}\".\"__drizzle_migrations\" (hash, created_at) \
6968 VALUES ('half_hash', 1680271923456);"
6969 ))
6970 .expect("reproduce the half-upgraded table");
6971 drop(client);
6972
6973 let applied = Migration::with_hash(
6974 "20230331141203_cli_half",
6975 "half_hash",
6976 1_680_271_923_000,
6977 vec![],
6978 );
6979 let broken = Migration::new(
6980 "20230401000000_cli_broken",
6981 &format!("INSERT INTO \"{migration_schema}\".missing_table VALUES (1);"),
6982 );
6983 let tracking = drizzle_migrations::Tracking::POSTGRES.schema(migration_schema.clone());
6984
6985 let set =
6986 Migrations::with_tracking(vec![applied.clone()], Dialect::PostgreSQL, tracking.clone());
6987 let result = run_postgres_sync_migrations(&set, &creds, false)
6988 .expect("a half-upgraded tracking table is completed");
6989 assert_eq!(result.applied_count, 0);
6990
6991 let set = Migrations::with_tracking(vec![applied, broken], Dialect::PostgreSQL, tracking);
6992 let error = run_postgres_sync_migrations(&set, &creds, false)
6993 .expect_err("the broken migration fails")
6994 .to_string();
6995 assert!(error.contains("20230401000000_cli_broken"), "{error}");
6996 assert!(
6997 error.contains("does not exist"),
6998 "server message is kept: {error}"
6999 );
7000
7001 let mut verify_client = connect_postgres_sync(&creds).expect("reconnect");
7002 let row = verify_client
7003 .query_one(
7004 &format!(
7005 "SELECT name, applied_at IS NOT NULL FROM \"{migration_schema}\".\"__drizzle_migrations\""
7006 ),
7007 &[],
7008 )
7009 .expect("upgraded row");
7010 assert_eq!(row.get::<_, String>(0), "20230331141203_cli_half");
7011 assert!(row.get::<_, bool>(1));
7012 let _ = verify_client.batch_execute(&format!(
7013 "DROP SCHEMA IF EXISTS \"{migration_schema}\" CASCADE;"
7014 ));
7015 }
7016
7017 #[cfg(feature = "postgres-sync")]
7020 #[test]
7021 fn postgres_sync_concurrent_first_migrate_creates_tracking_schema_once() {
7022 use drizzle_migrations::{Migration, Migrations};
7023 use drizzle_types::Dialect;
7024
7025 let creds = test_postgres_creds();
7026 let migration_schema = unique_pg_name("cli_sync_concurrent_first");
7027 let table = unique_pg_name("cli_sync_concurrent_table");
7028 let set = Migrations::with_tracking(
7029 vec![Migration::new(
7030 "20240101000000_cli_concurrent",
7031 &format!("CREATE TABLE \"{table}\" (id integer);"),
7032 )],
7033 Dialect::PostgreSQL,
7034 drizzle_migrations::Tracking::POSTGRES.schema(migration_schema.clone()),
7035 );
7036
7037 let barrier = std::sync::Barrier::new(6);
7038 let applied: usize = std::thread::scope(|scope| {
7039 let handles = (0..6)
7040 .map(|_| {
7041 scope.spawn(|| {
7042 barrier.wait();
7043 run_postgres_sync_migrations(&set, &creds, false)
7044 .map(|result| result.applied_count)
7045 .map_err(|error| error.to_string())
7046 })
7047 })
7048 .collect::<Vec<_>>();
7049 handles
7050 .into_iter()
7051 .map(|handle| {
7052 handle
7053 .join()
7054 .expect("thread")
7055 .unwrap_or_else(|error| panic!("{error}"))
7056 })
7057 .sum()
7058 });
7059 assert_eq!(applied, 1);
7060
7061 let mut verify_client = connect_postgres_sync(&creds).expect("reconnect");
7062 let _ = verify_client.batch_execute(&format!(
7063 "DROP TABLE IF EXISTS \"{table}\" CASCADE; \
7064 DROP SCHEMA IF EXISTS \"{migration_schema}\" CASCADE;"
7065 ));
7066 }
7067
7068 #[cfg(feature = "postgres-sync")]
7069 #[test]
7070 fn postgres_sync_migrate_upgrade_rejects_unmatched_legacy_rows() {
7071 use drizzle_migrations::{Migration, Migrations};
7072 use drizzle_types::Dialect;
7073
7074 let creds = test_postgres_creds();
7075 let mut client =
7076 connect_postgres_sync(&creds).expect("connect postgres for unmatched legacy row test");
7077
7078 let migration_schema = unique_pg_name("cli_sync_tracking_unmatched");
7079 client
7080 .batch_execute(&format!(
7081 "DROP SCHEMA IF EXISTS \"{migration_schema}\" CASCADE; \
7082 CREATE SCHEMA \"{migration_schema}\"; \
7083 CREATE TABLE \"{migration_schema}\".\"__drizzle_migrations\" (id SERIAL PRIMARY KEY, hash TEXT NOT NULL, created_at BIGINT);"
7084 ))
7085 .expect("setup unmatched legacy metadata");
7086
7087 let migration = Migration::new(
7088 "20230331141203_cli_sync_first",
7089 "CREATE TABLE \"cli_sync_unmatched_target\" (id integer primary key);",
7090 );
7091 let set = Migrations::with_tracking(
7092 vec![migration],
7093 Dialect::PostgreSQL,
7094 drizzle_migrations::Tracking::POSTGRES.schema(migration_schema.clone()),
7095 );
7096
7097 client
7098 .execute(
7099 &format!(
7100 "INSERT INTO \"{migration_schema}\".\"__drizzle_migrations\" (hash, created_at) VALUES ($1, $2)"
7101 ),
7102 &[&"unknown_hash", &1_680_271_924_000_i64],
7103 )
7104 .expect("insert unmatched legacy row");
7105 drop(client);
7106
7107 let err = run_postgres_sync_migrations(&set, &creds, false)
7108 .expect_err("unmatched legacy metadata should fail");
7109 assert!(err.to_string().contains("do not match local migrations"));
7110
7111 let mut verify_client = connect_postgres_sync(&creds).expect("reconnect for verification");
7112 let columns: Vec<String> = verify_client
7113 .query(
7114 "SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = '__drizzle_migrations' ORDER BY ordinal_position",
7115 &[&migration_schema],
7116 )
7117 .expect("query legacy tracking columns")
7118 .into_iter()
7119 .map(|row| row.get(0))
7120 .collect();
7121 assert_eq!(columns, vec!["id", "hash", "created_at"]);
7122
7123 let _ = verify_client.batch_execute(&format!(
7124 "DROP TABLE IF EXISTS \"cli_sync_unmatched_target\" CASCADE; \
7125 DROP SCHEMA IF EXISTS \"{migration_schema}\" CASCADE;"
7126 ));
7127 }
7128
7129 #[cfg(feature = "tokio-postgres")]
7130 #[test]
7131 fn tokio_postgres_migrate_applies_concurrent_index_without_transaction() {
7132 use drizzle_migrations::{Migration, Migrations};
7133 use drizzle_types::Dialect;
7134
7135 let creds = test_postgres_creds();
7136 let rt = tokio::runtime::Builder::new_current_thread()
7137 .enable_all()
7138 .build()
7139 .expect("create tokio runtime");
7140
7141 rt.block_on(async {
7142 let client = match connect_postgres_async(&creds).await {
7143 Ok(client) => client,
7144 Err(e) => {
7145 eprintln!(
7146 "Skipping tokio_postgres_migrate_applies_concurrent_index_without_transaction: {}",
7147 e
7148 );
7149 return;
7150 }
7151 };
7152
7153 let table = unique_pg_name("cli_async_users");
7154 let index = format!("{}_email_idx", table);
7155 let migration_schema = unique_pg_name("cli_async_mig");
7156
7157 client
7158 .batch_execute(&format!(
7159 "DROP TABLE IF EXISTS \"{table}\" CASCADE; \
7160 CREATE TABLE \"{table}\" (id integer, email text NOT NULL); \
7161 INSERT INTO \"{table}\" (id, email) VALUES (1, 'a@example.com');"
7162 ))
7163 .await
7164 .expect("setup table for async concurrent index test");
7165
7166 let migration_tag = format!("20260212000000_{table}");
7167 let migration_sql = format!(
7168 "CREATE INDEX CONCURRENTLY \"{index}\" ON \"{table}\" (\"email\");"
7169 );
7170 let set = Migrations::with_tracking(
7171 vec![Migration::new(&migration_tag, &migration_sql)],
7172 Dialect::PostgreSQL,
7173 drizzle_migrations::Tracking::POSTGRES.schema(migration_schema.clone()),
7174 );
7175
7176 let result = run_postgres_async_inner(&set, &creds, false)
7177 .await
7178 .expect("async migration with concurrent index should succeed");
7179 assert_eq!(result.applied_count, 1);
7180
7181 let exists: i64 = client
7182 .query_one(
7183 "SELECT COUNT(*)::bigint FROM pg_indexes \
7184 WHERE schemaname = 'public' AND tablename = $1 AND indexname = $2",
7185 &[&table, &index],
7186 )
7187 .await
7188 .expect("query pg_indexes")
7189 .get(0);
7190 assert_eq!(exists, 1, "async concurrent index was not created");
7191
7192 let _ = client
7193 .batch_execute(&format!(
7194 "DROP TABLE IF EXISTS \"{table}\" CASCADE; \
7195 DROP SCHEMA IF EXISTS \"{migration_schema}\" CASCADE;"
7196 ))
7197 .await;
7198 });
7199 }
7200
7201 #[cfg(feature = "tokio-postgres")]
7202 #[test]
7203 fn tokio_postgres_migrate_upgrades_legacy_tracking_table_and_applies_pending() {
7204 use drizzle_migrations::{Migration, Migrations};
7205 use drizzle_types::Dialect;
7206
7207 let creds = test_postgres_creds();
7208 let rt = tokio::runtime::Builder::new_current_thread()
7209 .enable_all()
7210 .build()
7211 .expect("create tokio runtime");
7212
7213 rt.block_on(async {
7214 let client = connect_postgres_async(&creds)
7215 .await
7216 .expect("connect tokio-postgres for legacy tracking upgrade test");
7217
7218 let applied_table = unique_pg_name("cli_async_applied");
7219 let pending_table = unique_pg_name("cli_async_pending");
7220 let migration_schema = unique_pg_name("cli_async_tracking");
7221
7222 client
7223 .batch_execute(&format!(
7224 "DROP TABLE IF EXISTS \"{applied_table}\" CASCADE; \
7225 DROP TABLE IF EXISTS \"{pending_table}\" CASCADE; \
7226 DROP SCHEMA IF EXISTS \"{migration_schema}\" CASCADE; \
7227 CREATE SCHEMA \"{migration_schema}\"; \
7228 CREATE TABLE \"{migration_schema}\".\"__drizzle_migrations\" (id SERIAL PRIMARY KEY, hash TEXT NOT NULL, created_at BIGINT); \
7229 CREATE TABLE \"{applied_table}\" (id integer primary key);"
7230 ))
7231 .await
7232 .expect("setup legacy postgres tracking metadata");
7233
7234 let first = Migration::new(
7235 &format!("20230331141203_{applied_table}"),
7236 &format!("CREATE TABLE \"{applied_table}\" (id integer primary key);"),
7237 );
7238 let second = Migration::new(
7239 &format!("20230331141204_{pending_table}"),
7240 &format!("CREATE TABLE \"{pending_table}\" (id integer primary key);"),
7241 );
7242 let set = Migrations::with_tracking(
7243 vec![first.clone(), second.clone()],
7244 Dialect::PostgreSQL,
7245 drizzle_migrations::Tracking::POSTGRES.schema(migration_schema.clone()),
7246 );
7247
7248 client
7249 .execute(
7250 &format!(
7251 "INSERT INTO \"{migration_schema}\".\"__drizzle_migrations\" (hash, created_at) VALUES ($1, $2)"
7252 ),
7253 &[&first.hash(), &first.created_at()],
7254 )
7255 .await
7256 .expect("insert legacy applied migration row");
7257
7258 let result = run_postgres_async_inner(&set, &creds, false)
7259 .await
7260 .expect("async migration upgrade should succeed");
7261 assert_eq!(result.applied_count, 1);
7262 assert_eq!(result.applied_migrations, vec![second.hash().to_string()]);
7263
7264 let columns: Vec<String> = client
7265 .query(
7266 "SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = '__drizzle_migrations' ORDER BY ordinal_position",
7267 &[&migration_schema],
7268 )
7269 .await
7270 .expect("query upgraded tracking columns")
7271 .into_iter()
7272 .map(|row| row.get(0))
7273 .collect();
7274 assert_eq!(
7275 columns,
7276 vec!["id", "hash", "created_at", "name", "applied_at"]
7277 );
7278
7279 let rows: Vec<(String, i64, String, Option<String>)> = client
7280 .query(
7281 &format!(
7282 "SELECT hash, created_at, name, applied_at::text FROM \"{migration_schema}\".\"__drizzle_migrations\" ORDER BY id ASC"
7283 ),
7284 &[],
7285 )
7286 .await
7287 .expect("query upgraded metadata rows")
7288 .into_iter()
7289 .map(|row| (row.get(0), row.get(1), row.get(2), row.get(3)))
7290 .collect();
7291 assert_eq!(rows.len(), 2);
7292 assert_eq!(rows[0].0, first.hash());
7293 assert_eq!(rows[0].1, first.created_at());
7294 assert_eq!(rows[0].2, first.name());
7295 assert!(
7296 rows[0].3.is_some(),
7297 "legacy rows get applied_at backfilled so they cannot read as interrupted"
7298 );
7299 assert_eq!(rows[1].0, second.hash());
7300 assert_eq!(rows[1].1, second.created_at());
7301 assert_eq!(rows[1].2, second.name());
7302 assert!(rows[1].3.is_some());
7303
7304 let pending_exists: i64 = client
7305 .query_one(
7306 "SELECT COUNT(*)::bigint FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1",
7307 &[&pending_table],
7308 )
7309 .await
7310 .expect("query pending table")
7311 .get(0);
7312 assert_eq!(pending_exists, 1);
7313
7314 let _ = client
7315 .batch_execute(&format!(
7316 "DROP TABLE IF EXISTS \"{applied_table}\" CASCADE; \
7317 DROP TABLE IF EXISTS \"{pending_table}\" CASCADE; \
7318 DROP SCHEMA IF EXISTS \"{migration_schema}\" CASCADE;"
7319 ))
7320 .await;
7321 });
7322 }
7323}