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