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