1use std::collections::{HashMap, HashSet};
13
14use serde::Serialize;
15
16#[cfg(feature = "postgres")]
17use tokio_postgres::Client;
18
19use crate::db::{DbClient, quote_ident};
20use crate::dialect::DialectKind;
21use crate::error::Result;
22#[cfg(any(not(feature = "postgres"), not(feature = "mysql")))]
23use crate::error::WaypointError;
24
25#[derive(Debug, Clone, Serialize, PartialEq)]
31pub struct SchemaSnapshot {
32 pub tables: Vec<TableDef>,
34 pub views: Vec<ViewDef>,
36 pub indexes: Vec<IndexDef>,
38 pub sequences: Vec<SequenceDef>,
40 pub functions: Vec<FunctionDef>,
42 pub enums: Vec<EnumDef>,
44 pub constraints: Vec<ConstraintDef>,
46 pub triggers: Vec<TriggerDef>,
48 pub extensions: Vec<String>,
50}
51
52#[derive(Debug, Clone, Serialize, PartialEq)]
54pub struct TableDef {
55 pub schema: String,
57 pub name: String,
59 pub columns: Vec<ColumnDef>,
61}
62
63#[derive(Debug, Clone, Serialize, PartialEq)]
65pub struct ColumnDef {
66 pub name: String,
68 pub data_type: String,
70 pub is_nullable: bool,
72 pub default: Option<String>,
74 pub ordinal_position: i32,
76}
77
78#[derive(Debug, Clone, Serialize, PartialEq)]
80pub struct ViewDef {
81 pub schema: String,
83 pub name: String,
85 pub definition: String,
87 pub is_materialized: bool,
89}
90
91#[derive(Debug, Clone, Serialize, PartialEq)]
93pub struct IndexDef {
94 pub schema: String,
96 pub name: String,
98 pub table_name: String,
100 pub definition: String,
102 pub is_unique: bool,
104}
105
106#[derive(Debug, Clone, Serialize, PartialEq)]
108pub struct SequenceDef {
109 pub schema: String,
111 pub name: String,
113 pub data_type: String,
115}
116
117#[derive(Debug, Clone, Serialize, PartialEq)]
119pub struct FunctionDef {
120 pub schema: String,
122 pub name: String,
124 pub arguments: String,
126 pub return_type: String,
128 pub language: String,
130 pub definition: String,
132}
133
134#[derive(Debug, Clone, Serialize, PartialEq)]
136pub struct EnumDef {
137 pub schema: String,
139 pub name: String,
141 pub values: Vec<String>,
143}
144
145#[derive(Debug, Clone, Serialize, PartialEq)]
147pub struct ConstraintDef {
148 pub schema: String,
150 pub table_name: String,
152 pub name: String,
154 pub constraint_type: String,
156 pub definition: String,
158}
159
160#[derive(Debug, Clone, Serialize, PartialEq)]
162pub struct TriggerDef {
163 pub schema: String,
165 pub table_name: String,
167 pub name: String,
169 pub definition: String,
171}
172
173#[derive(Debug, Clone, Serialize)]
175pub enum SchemaDiff {
176 TableAdded(TableDef),
178 TableDropped(String),
180 ColumnAdded { table: String, column: ColumnDef },
182 ColumnDropped { table: String, column: String },
184 ColumnAltered {
186 table: String,
187 column: String,
188 from: ColumnDef,
189 to: ColumnDef,
190 },
191 IndexAdded(IndexDef),
193 IndexDropped { name: String, table_name: String },
199 ViewAdded(ViewDef),
201 ViewDropped(String),
203 ViewAltered {
205 name: String,
206 from: String,
207 to: String,
208 },
209 SequenceAdded(SequenceDef),
211 SequenceDropped(String),
213 FunctionAdded(FunctionDef),
215 FunctionDropped(String),
217 FunctionAltered { name: String },
219 EnumAdded(EnumDef),
221 EnumDropped(String),
223 ConstraintAdded(ConstraintDef),
225 ConstraintDropped { table: String, name: String },
227 TriggerAdded(TriggerDef),
229 TriggerDropped { table: String, name: String },
231 ExtensionAdded(String),
233 ExtensionDropped(String),
235}
236
237impl std::fmt::Display for SchemaDiff {
238 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239 match self {
240 SchemaDiff::TableAdded(t) => write!(f, "+ TABLE {}", t.name),
241 SchemaDiff::TableDropped(n) => write!(f, "- TABLE {}", n),
242 SchemaDiff::ColumnAdded { table, column } => {
243 write!(
244 f,
245 "+ COLUMN {}.{} ({})",
246 table, column.name, column.data_type
247 )
248 }
249 SchemaDiff::ColumnDropped { table, column } => {
250 write!(f, "- COLUMN {}.{}", table, column)
251 }
252 SchemaDiff::ColumnAltered { table, column, .. } => {
253 write!(f, "~ COLUMN {}.{}", table, column)
254 }
255 SchemaDiff::IndexAdded(idx) => write!(f, "+ INDEX {}", idx.name),
256 SchemaDiff::IndexDropped { name, table_name } => {
257 write!(f, "- INDEX {} ON {}", name, table_name)
258 }
259 SchemaDiff::ViewAdded(v) => write!(f, "+ VIEW {}", v.name),
260 SchemaDiff::ViewDropped(n) => write!(f, "- VIEW {}", n),
261 SchemaDiff::ViewAltered { name, .. } => write!(f, "~ VIEW {}", name),
262 SchemaDiff::SequenceAdded(s) => write!(f, "+ SEQUENCE {}", s.name),
263 SchemaDiff::SequenceDropped(n) => write!(f, "- SEQUENCE {}", n),
264 SchemaDiff::FunctionAdded(func) => write!(f, "+ FUNCTION {}", func.name),
265 SchemaDiff::FunctionDropped(n) => write!(f, "- FUNCTION {}", n),
266 SchemaDiff::FunctionAltered { name } => write!(f, "~ FUNCTION {}", name),
267 SchemaDiff::EnumAdded(e) => write!(f, "+ TYPE {} (enum)", e.name),
268 SchemaDiff::EnumDropped(n) => write!(f, "- TYPE {} (enum)", n),
269 SchemaDiff::ConstraintAdded(c) => {
270 write!(f, "+ CONSTRAINT {} ON {}", c.name, c.table_name)
271 }
272 SchemaDiff::ConstraintDropped { table, name } => {
273 write!(f, "- CONSTRAINT {} ON {}", name, table)
274 }
275 SchemaDiff::TriggerAdded(t) => write!(f, "+ TRIGGER {} ON {}", t.name, t.table_name),
276 SchemaDiff::TriggerDropped { table, name } => {
277 write!(f, "- TRIGGER {} ON {}", name, table)
278 }
279 SchemaDiff::ExtensionAdded(n) => write!(f, "+ EXTENSION {}", n),
280 SchemaDiff::ExtensionDropped(n) => write!(f, "- EXTENSION {}", n),
281 }
282 }
283}
284
285pub async fn introspect_db(client: &DbClient, schema: &str) -> Result<SchemaSnapshot> {
287 match client.dialect_kind() {
288 #[cfg(feature = "postgres")]
289 DialectKind::Postgres => introspect(client.as_postgres()?, schema).await,
290 #[cfg(not(feature = "postgres"))]
291 DialectKind::Postgres => Err(WaypointError::ConfigError(
292 "PostgreSQL support is not compiled in".into(),
293 )),
294 #[cfg(feature = "mysql")]
295 DialectKind::Mysql => introspect_mysql(client, schema).await,
296 #[cfg(not(feature = "mysql"))]
297 DialectKind::Mysql => Err(WaypointError::ConfigError(
298 "MySQL support is not compiled in".into(),
299 )),
300 }
301}
302
303#[cfg(feature = "postgres")]
305pub async fn introspect(client: &Client, schema: &str) -> Result<SchemaSnapshot> {
306 let (tables, views, indexes, sequences, functions, enums, constraints, triggers, extensions) =
307 tokio::try_join!(
308 introspect_tables(client, schema),
309 introspect_views(client, schema),
310 introspect_indexes(client, schema),
311 introspect_sequences(client, schema),
312 introspect_functions(client, schema),
313 introspect_enums(client, schema),
314 introspect_constraints(client, schema),
315 introspect_triggers(client, schema),
316 introspect_extensions(client),
317 )?;
318
319 Ok(SchemaSnapshot {
320 tables,
321 views,
322 indexes,
323 sequences,
324 functions,
325 enums,
326 constraints,
327 triggers,
328 extensions,
329 })
330}
331
332#[cfg(feature = "postgres")]
333async fn introspect_tables(client: &Client, schema: &str) -> Result<Vec<TableDef>> {
334 let rows = client
335 .query(
336 "SELECT t.table_name, c.column_name, c.data_type, c.is_nullable, c.column_default, c.ordinal_position
337 FROM information_schema.tables t
338 LEFT JOIN information_schema.columns c
339 ON t.table_schema = c.table_schema AND t.table_name = c.table_name
340 WHERE t.table_schema = $1 AND t.table_type = 'BASE TABLE'
341 ORDER BY t.table_name, c.ordinal_position",
342 &[&schema],
343 )
344 .await?;
345
346 let mut tables: Vec<TableDef> = Vec::new();
347 let mut current_table: Option<String> = None;
348 let mut columns: Vec<ColumnDef> = Vec::new();
349
350 for row in &rows {
351 let table_name: String = row.get(0);
352 let col_name: Option<String> = row.get(1);
353
354 if current_table.as_ref() != Some(&table_name) {
355 if let Some(prev_name) = current_table.take() {
356 tables.push(TableDef {
357 schema: schema.to_string(),
358 name: prev_name,
359 columns: std::mem::take(&mut columns),
360 });
361 }
362 current_table = Some(table_name.clone());
363 }
364
365 if let Some(name) = col_name {
366 columns.push(ColumnDef {
367 name,
368 data_type: row.get(2),
369 is_nullable: row.get::<_, String>(3) == "YES",
370 default: row.get(4),
371 ordinal_position: row.get(5),
372 });
373 }
374 }
375
376 if let Some(name) = current_table {
378 tables.push(TableDef {
379 schema: schema.to_string(),
380 name,
381 columns,
382 });
383 }
384
385 Ok(tables)
386}
387
388#[cfg(feature = "postgres")]
389async fn introspect_views(client: &Client, schema: &str) -> Result<Vec<ViewDef>> {
390 let rows = client
392 .query(
393 "SELECT table_name, view_definition
394 FROM information_schema.views
395 WHERE table_schema = $1
396 ORDER BY table_name",
397 &[&schema],
398 )
399 .await?;
400
401 let mut views: Vec<ViewDef> = rows
402 .iter()
403 .map(|r| ViewDef {
404 schema: schema.to_string(),
405 name: r.get(0),
406 definition: r.get::<_, Option<String>>(1).unwrap_or_default(),
407 is_materialized: false,
408 })
409 .collect();
410
411 let mat_rows = client
413 .query(
414 "SELECT c.relname, pg_get_viewdef(c.oid)
415 FROM pg_class c
416 JOIN pg_namespace n ON n.oid = c.relnamespace
417 WHERE n.nspname = $1 AND c.relkind = 'm'
418 ORDER BY c.relname",
419 &[&schema],
420 )
421 .await?;
422
423 for r in &mat_rows {
424 views.push(ViewDef {
425 schema: schema.to_string(),
426 name: r.get(0),
427 definition: r.get::<_, Option<String>>(1).unwrap_or_default(),
428 is_materialized: true,
429 });
430 }
431
432 Ok(views)
433}
434
435#[cfg(feature = "postgres")]
436async fn introspect_indexes(client: &Client, schema: &str) -> Result<Vec<IndexDef>> {
437 let rows = client
438 .query(
439 "SELECT indexname, tablename, indexdef
440 FROM pg_indexes
441 WHERE schemaname = $1
442 ORDER BY indexname",
443 &[&schema],
444 )
445 .await?;
446
447 Ok(rows
448 .iter()
449 .map(|r| {
450 let definition: String = r.get(2);
451 IndexDef {
452 schema: schema.to_string(),
453 name: r.get(0),
454 table_name: r.get(1),
455 is_unique: definition.to_uppercase().contains("UNIQUE"),
456 definition,
457 }
458 })
459 .collect())
460}
461
462#[cfg(feature = "postgres")]
463async fn introspect_sequences(client: &Client, schema: &str) -> Result<Vec<SequenceDef>> {
464 let rows = client
465 .query(
466 "SELECT sequence_name, data_type
467 FROM information_schema.sequences
468 WHERE sequence_schema = $1
469 ORDER BY sequence_name",
470 &[&schema],
471 )
472 .await?;
473
474 Ok(rows
475 .iter()
476 .map(|r| SequenceDef {
477 schema: schema.to_string(),
478 name: r.get(0),
479 data_type: r.get(1),
480 })
481 .collect())
482}
483
484#[cfg(feature = "postgres")]
485async fn introspect_functions(client: &Client, schema: &str) -> Result<Vec<FunctionDef>> {
486 let rows = client
487 .query(
488 "SELECT p.proname,
489 pg_get_function_arguments(p.oid),
490 pg_get_function_result(p.oid),
491 l.lanname,
492 pg_get_functiondef(p.oid)
493 FROM pg_proc p
494 JOIN pg_namespace n ON n.oid = p.pronamespace
495 JOIN pg_language l ON l.oid = p.prolang
496 WHERE n.nspname = $1
497 AND p.prokind IN ('f', 'p')
498 ORDER BY p.proname",
499 &[&schema],
500 )
501 .await?;
502
503 Ok(rows
504 .iter()
505 .map(|r| FunctionDef {
506 schema: schema.to_string(),
507 name: r.get(0),
508 arguments: r.get(1),
509 return_type: r.get::<_, Option<String>>(2).unwrap_or_default(),
510 language: r.get(3),
511 definition: r.get::<_, Option<String>>(4).unwrap_or_default(),
512 })
513 .collect())
514}
515
516#[cfg(feature = "postgres")]
517async fn introspect_enums(client: &Client, schema: &str) -> Result<Vec<EnumDef>> {
518 let rows = client
519 .query(
520 "SELECT t.typname, array_agg(e.enumlabel ORDER BY e.enumsortorder)::text[]
521 FROM pg_type t
522 JOIN pg_enum e ON e.enumtypid = t.oid
523 JOIN pg_namespace n ON n.oid = t.typnamespace
524 WHERE n.nspname = $1
525 GROUP BY t.typname
526 ORDER BY t.typname",
527 &[&schema],
528 )
529 .await?;
530
531 Ok(rows
532 .iter()
533 .map(|r| EnumDef {
534 schema: schema.to_string(),
535 name: r.get(0),
536 values: r.get(1),
537 })
538 .collect())
539}
540
541#[cfg(feature = "postgres")]
542async fn introspect_constraints(client: &Client, schema: &str) -> Result<Vec<ConstraintDef>> {
543 let rows = client
544 .query(
545 "SELECT tc.table_name, tc.constraint_name, tc.constraint_type,
546 pg_get_constraintdef(c.oid)
547 FROM information_schema.table_constraints tc
548 JOIN pg_constraint c ON c.conname = tc.constraint_name
549 JOIN pg_namespace n ON n.oid = c.connamespace
550 WHERE tc.constraint_schema = $1 AND n.nspname = $1
551 ORDER BY tc.table_name, tc.constraint_name",
552 &[&schema],
553 )
554 .await?;
555
556 Ok(rows
557 .iter()
558 .map(|r| ConstraintDef {
559 schema: schema.to_string(),
560 table_name: r.get(0),
561 name: r.get(1),
562 constraint_type: r.get(2),
563 definition: r.get::<_, Option<String>>(3).unwrap_or_default(),
564 })
565 .collect())
566}
567
568#[cfg(feature = "postgres")]
569async fn introspect_triggers(client: &Client, schema: &str) -> Result<Vec<TriggerDef>> {
570 let rows = client
571 .query(
572 "SELECT event_object_table, trigger_name, action_statement
573 FROM information_schema.triggers
574 WHERE trigger_schema = $1
575 ORDER BY event_object_table, trigger_name",
576 &[&schema],
577 )
578 .await?;
579
580 Ok(rows
581 .iter()
582 .map(|r| TriggerDef {
583 schema: schema.to_string(),
584 table_name: r.get(0),
585 name: r.get(1),
586 definition: r.get(2),
587 })
588 .collect())
589}
590
591#[cfg(feature = "postgres")]
592async fn introspect_extensions(client: &Client) -> Result<Vec<String>> {
593 let rows = client
594 .query(
595 "SELECT extname FROM pg_extension WHERE extname != 'plpgsql' ORDER BY extname",
596 &[],
597 )
598 .await?;
599
600 Ok(rows.iter().map(|r| r.get(0)).collect())
601}
602
603pub fn diff(before: &SchemaSnapshot, after: &SchemaSnapshot) -> Vec<SchemaDiff> {
605 let mut diffs = Vec::new();
606
607 let before_tables: HashMap<&str, &TableDef> =
611 before.tables.iter().map(|t| (t.name.as_str(), t)).collect();
612 let after_tables: HashMap<&str, &TableDef> =
613 after.tables.iter().map(|t| (t.name.as_str(), t)).collect();
614
615 let before_views: HashMap<&str, &ViewDef> =
617 before.views.iter().map(|v| (v.name.as_str(), v)).collect();
618 let after_views: HashMap<&str, &ViewDef> =
619 after.views.iter().map(|v| (v.name.as_str(), v)).collect();
620
621 let before_indexes: HashSet<&str> = before.indexes.iter().map(|i| i.name.as_str()).collect();
623 let after_indexes: HashSet<&str> = after.indexes.iter().map(|i| i.name.as_str()).collect();
624
625 let before_sequences: HashSet<&str> =
627 before.sequences.iter().map(|s| s.name.as_str()).collect();
628 let after_sequences: HashSet<&str> = after.sequences.iter().map(|s| s.name.as_str()).collect();
629
630 let before_functions: HashMap<&str, &FunctionDef> = before
632 .functions
633 .iter()
634 .map(|f| (f.name.as_str(), f))
635 .collect();
636 let after_functions: HashMap<&str, &FunctionDef> = after
637 .functions
638 .iter()
639 .map(|f| (f.name.as_str(), f))
640 .collect();
641
642 let before_enums: HashSet<&str> = before.enums.iter().map(|e| e.name.as_str()).collect();
644 let after_enums: HashSet<&str> = after.enums.iter().map(|e| e.name.as_str()).collect();
645
646 let before_constraints: HashSet<(&str, &str)> = before
648 .constraints
649 .iter()
650 .map(|c| (c.table_name.as_str(), c.name.as_str()))
651 .collect();
652 let after_constraints: HashSet<(&str, &str)> = after
653 .constraints
654 .iter()
655 .map(|c| (c.table_name.as_str(), c.name.as_str()))
656 .collect();
657
658 let before_triggers: HashSet<(&str, &str)> = before
660 .triggers
661 .iter()
662 .map(|t| (t.table_name.as_str(), t.name.as_str()))
663 .collect();
664 let after_triggers: HashSet<(&str, &str)> = after
665 .triggers
666 .iter()
667 .map(|t| (t.table_name.as_str(), t.name.as_str()))
668 .collect();
669
670 let before_extensions: HashSet<&str> = before.extensions.iter().map(|e| e.as_str()).collect();
672 let after_extensions: HashSet<&str> = after.extensions.iter().map(|e| e.as_str()).collect();
673
674 for bt in &before.tables {
676 if let Some(at) = after_tables.get(bt.name.as_str()) {
677 diff_columns(&mut diffs, &bt.name, &bt.columns, &at.columns);
678 } else {
679 diffs.push(SchemaDiff::TableDropped(bt.name.clone()));
680 }
681 }
682 for at in &after.tables {
683 if !before_tables.contains_key(at.name.as_str()) {
684 diffs.push(SchemaDiff::TableAdded(at.clone()));
685 }
686 }
687
688 for bv in &before.views {
690 if let Some(av) = after_views.get(bv.name.as_str()) {
691 if bv.definition != av.definition {
692 diffs.push(SchemaDiff::ViewAltered {
693 name: bv.name.clone(),
694 from: bv.definition.clone(),
695 to: av.definition.clone(),
696 });
697 }
698 } else {
699 diffs.push(SchemaDiff::ViewDropped(bv.name.clone()));
700 }
701 }
702 for av in &after.views {
703 if !before_views.contains_key(av.name.as_str()) {
704 diffs.push(SchemaDiff::ViewAdded(av.clone()));
705 }
706 }
707
708 for bi in &before.indexes {
710 if !after_indexes.contains(bi.name.as_str()) {
711 diffs.push(SchemaDiff::IndexDropped {
712 name: bi.name.clone(),
713 table_name: bi.table_name.clone(),
714 });
715 }
716 }
717 for ai in &after.indexes {
718 if !before_indexes.contains(ai.name.as_str()) {
719 diffs.push(SchemaDiff::IndexAdded(ai.clone()));
720 }
721 }
722
723 for bs in &before.sequences {
725 if !after_sequences.contains(bs.name.as_str()) {
726 diffs.push(SchemaDiff::SequenceDropped(bs.name.clone()));
727 }
728 }
729 for a_s in &after.sequences {
730 if !before_sequences.contains(a_s.name.as_str()) {
731 diffs.push(SchemaDiff::SequenceAdded(a_s.clone()));
732 }
733 }
734
735 for bf in &before.functions {
737 if let Some(af) = after_functions.get(bf.name.as_str()) {
738 if bf.definition != af.definition {
739 diffs.push(SchemaDiff::FunctionAltered {
740 name: bf.name.clone(),
741 });
742 }
743 } else {
744 diffs.push(SchemaDiff::FunctionDropped(bf.name.clone()));
745 }
746 }
747 for af in &after.functions {
748 if !before_functions.contains_key(af.name.as_str()) {
749 diffs.push(SchemaDiff::FunctionAdded(af.clone()));
750 }
751 }
752
753 for be in &before.enums {
755 if !after_enums.contains(be.name.as_str()) {
756 diffs.push(SchemaDiff::EnumDropped(be.name.clone()));
757 }
758 }
759 for ae in &after.enums {
760 if !before_enums.contains(ae.name.as_str()) {
761 diffs.push(SchemaDiff::EnumAdded(ae.clone()));
762 }
763 }
764
765 for bc in &before.constraints {
767 if !after_constraints.contains(&(bc.table_name.as_str(), bc.name.as_str())) {
768 diffs.push(SchemaDiff::ConstraintDropped {
769 table: bc.table_name.clone(),
770 name: bc.name.clone(),
771 });
772 }
773 }
774 for ac in &after.constraints {
775 if !before_constraints.contains(&(ac.table_name.as_str(), ac.name.as_str())) {
776 diffs.push(SchemaDiff::ConstraintAdded(ac.clone()));
777 }
778 }
779
780 for bt in &before.triggers {
782 if !after_triggers.contains(&(bt.table_name.as_str(), bt.name.as_str())) {
783 diffs.push(SchemaDiff::TriggerDropped {
784 table: bt.table_name.clone(),
785 name: bt.name.clone(),
786 });
787 }
788 }
789 for at in &after.triggers {
790 if !before_triggers.contains(&(at.table_name.as_str(), at.name.as_str())) {
791 diffs.push(SchemaDiff::TriggerAdded(at.clone()));
792 }
793 }
794
795 for ext in &before.extensions {
797 if !after_extensions.contains(ext.as_str()) {
798 diffs.push(SchemaDiff::ExtensionDropped(ext.clone()));
799 }
800 }
801 for ext in &after.extensions {
802 if !before_extensions.contains(ext.as_str()) {
803 diffs.push(SchemaDiff::ExtensionAdded(ext.clone()));
804 }
805 }
806
807 diffs
808}
809
810fn diff_columns(
811 diffs: &mut Vec<SchemaDiff>,
812 table: &str,
813 before: &[ColumnDef],
814 after: &[ColumnDef],
815) {
816 let before_cols: HashMap<&str, &ColumnDef> =
817 before.iter().map(|c| (c.name.as_str(), c)).collect();
818 let after_cols: HashMap<&str, &ColumnDef> =
819 after.iter().map(|c| (c.name.as_str(), c)).collect();
820
821 for bc in before {
822 if let Some(ac) = after_cols.get(bc.name.as_str()) {
823 if bc != *ac {
824 diffs.push(SchemaDiff::ColumnAltered {
825 table: table.to_string(),
826 column: bc.name.clone(),
827 from: bc.clone(),
828 to: (*ac).clone(),
829 });
830 }
831 } else {
832 diffs.push(SchemaDiff::ColumnDropped {
833 table: table.to_string(),
834 column: bc.name.clone(),
835 });
836 }
837 }
838 for ac in after {
839 if !before_cols.contains_key(ac.name.as_str()) {
840 diffs.push(SchemaDiff::ColumnAdded {
841 table: table.to_string(),
842 column: ac.clone(),
843 });
844 }
845 }
846}
847
848fn ddl_rank(d: &SchemaDiff) -> u8 {
860 match d {
861 SchemaDiff::TriggerDropped { .. } => 0,
863 SchemaDiff::ViewDropped(_) => 1,
864 SchemaDiff::FunctionDropped(_) => 2,
865 SchemaDiff::IndexDropped { .. } => 3,
866 SchemaDiff::ConstraintDropped { .. } => 4,
867 SchemaDiff::ColumnDropped { .. } => 5,
868 SchemaDiff::TableDropped(_) => 6,
869 SchemaDiff::SequenceDropped(_) => 7,
870 SchemaDiff::EnumDropped(_) => 8,
871 SchemaDiff::ExtensionDropped(_) => 9,
872
873 SchemaDiff::ExtensionAdded(_) => 10,
875 SchemaDiff::EnumAdded(_) => 11,
876 SchemaDiff::SequenceAdded(_) => 12,
877 SchemaDiff::TableAdded(_) => 13,
878 SchemaDiff::ColumnAdded { .. } => 14,
879 SchemaDiff::ColumnAltered { .. } => 15,
880 SchemaDiff::ConstraintAdded(_) => 16,
881 SchemaDiff::IndexAdded(_) => 17,
882 SchemaDiff::ViewAdded(_) | SchemaDiff::ViewAltered { .. } => 18,
883 SchemaDiff::FunctionAdded(_) | SchemaDiff::FunctionAltered { .. } => 19,
884 SchemaDiff::TriggerAdded(_) => 20,
885 }
886}
887
888fn order_diffs_for_ddl(diffs: &[SchemaDiff]) -> Vec<&SchemaDiff> {
895 let constraint_names: std::collections::HashSet<&str> = diffs
896 .iter()
897 .filter_map(|d| match d {
898 SchemaDiff::ConstraintAdded(c) => Some(c.name.as_str()),
899 _ => None,
900 })
901 .collect();
902
903 let mut ordered: Vec<&SchemaDiff> = diffs
904 .iter()
905 .filter(|d| match d {
906 SchemaDiff::IndexAdded(idx) => !constraint_names.contains(idx.name.as_str()),
907 _ => true,
908 })
909 .collect();
910 ordered.sort_by_key(|d| ddl_rank(d));
911 ordered
912}
913
914pub fn generate_ddl(diffs: &[SchemaDiff]) -> String {
916 let mut statements = Vec::new();
917
918 for d in order_diffs_for_ddl(diffs) {
919 match d {
920 SchemaDiff::TableAdded(t) => {
921 let cols: Vec<String> = t
922 .columns
923 .iter()
924 .map(|c| {
925 let mut col = format!(" {} {}", quote_ident(&c.name), c.data_type);
926 if !c.is_nullable {
927 col.push_str(" NOT NULL");
928 }
929 if let Some(ref default) = c.default {
930 col.push_str(&format!(" DEFAULT {}", default));
931 }
932 col
933 })
934 .collect();
935 statements.push(format!(
936 "CREATE TABLE {} (\n{}\n);",
937 quote_ident(&t.name),
938 cols.join(",\n")
939 ));
940 }
941 SchemaDiff::TableDropped(name) => {
942 statements.push(format!(
943 "DROP TABLE IF EXISTS {} CASCADE;",
944 quote_ident(name)
945 ));
946 }
947 SchemaDiff::ColumnAdded { table, column } => {
948 let mut stmt = format!(
949 "ALTER TABLE {} ADD COLUMN {} {}",
950 quote_ident(table),
951 quote_ident(&column.name),
952 column.data_type
953 );
954 if !column.is_nullable {
955 stmt.push_str(" NOT NULL");
956 }
957 if let Some(ref default) = column.default {
958 stmt.push_str(&format!(" DEFAULT {}", default));
959 }
960 stmt.push(';');
961 statements.push(stmt);
962 }
963 SchemaDiff::ColumnDropped { table, column } => {
964 statements.push(format!(
965 "ALTER TABLE {} DROP COLUMN {};",
966 quote_ident(table),
967 quote_ident(column)
968 ));
969 }
970 SchemaDiff::ColumnAltered {
971 table, column, to, ..
972 } => {
973 statements.push(format!(
974 "ALTER TABLE {} ALTER COLUMN {} TYPE {};",
975 quote_ident(table),
976 quote_ident(column),
977 to.data_type
978 ));
979 if to.is_nullable {
980 statements.push(format!(
981 "ALTER TABLE {} ALTER COLUMN {} DROP NOT NULL;",
982 quote_ident(table),
983 quote_ident(column)
984 ));
985 } else {
986 statements.push(format!(
987 "ALTER TABLE {} ALTER COLUMN {} SET NOT NULL;",
988 quote_ident(table),
989 quote_ident(column)
990 ));
991 }
992 match &to.default {
993 Some(default) => {
994 statements.push(format!(
995 "ALTER TABLE {} ALTER COLUMN {} SET DEFAULT {};",
996 quote_ident(table),
997 quote_ident(column),
998 default
999 ));
1000 }
1001 None => {
1002 statements.push(format!(
1003 "ALTER TABLE {} ALTER COLUMN {} DROP DEFAULT;",
1004 quote_ident(table),
1005 quote_ident(column)
1006 ));
1007 }
1008 }
1009 }
1010 SchemaDiff::IndexAdded(idx) => {
1011 statements.push(format!("{};", idx.definition));
1012 }
1013 SchemaDiff::IndexDropped { name, .. } => {
1014 statements.push(format!("DROP INDEX IF EXISTS {};", quote_ident(name)));
1016 }
1017 SchemaDiff::ViewAdded(v) => {
1018 let keyword = if v.is_materialized {
1019 "MATERIALIZED VIEW"
1020 } else {
1021 "VIEW"
1022 };
1023 statements.push(format!(
1024 "CREATE {} {} AS {};",
1025 keyword,
1026 quote_ident(&v.name),
1027 v.definition.trim_end_matches(';').trim()
1028 ));
1029 }
1030 SchemaDiff::ViewDropped(name) => {
1031 statements.push(format!(
1032 "DROP VIEW IF EXISTS {} CASCADE;",
1033 quote_ident(name)
1034 ));
1035 }
1036 SchemaDiff::ViewAltered { name, to, .. } => {
1037 statements.push(format!(
1038 "CREATE OR REPLACE VIEW {} AS {};",
1039 quote_ident(name),
1040 to.trim_end_matches(';').trim()
1041 ));
1042 }
1043 SchemaDiff::SequenceAdded(s) => {
1044 statements.push(format!("CREATE SEQUENCE {};", quote_ident(&s.name)));
1045 }
1046 SchemaDiff::SequenceDropped(name) => {
1047 statements.push(format!("DROP SEQUENCE IF EXISTS {};", quote_ident(name)));
1048 }
1049 SchemaDiff::FunctionAdded(func) => {
1050 statements.push(format!("{};", func.definition.trim_end_matches(';')));
1051 }
1052 SchemaDiff::FunctionDropped(name) => {
1053 statements.push(format!(
1054 "DROP FUNCTION IF EXISTS {} CASCADE;",
1055 quote_ident(name)
1056 ));
1057 }
1058 SchemaDiff::FunctionAltered { name } => {
1059 statements.push(format!(
1061 "-- Function {} was altered; manual review needed",
1062 name
1063 ));
1064 }
1065 SchemaDiff::EnumAdded(e) => {
1066 let values: Vec<String> = e
1067 .values
1068 .iter()
1069 .map(|v| crate::db::quote_literal(v))
1070 .collect();
1071 statements.push(format!(
1072 "CREATE TYPE {} AS ENUM ({});",
1073 quote_ident(&e.name),
1074 values.join(", ")
1075 ));
1076 }
1077 SchemaDiff::EnumDropped(name) => {
1078 statements.push(format!(
1079 "DROP TYPE IF EXISTS {} CASCADE;",
1080 quote_ident(name)
1081 ));
1082 }
1083 SchemaDiff::ConstraintAdded(c) => {
1084 statements.push(format!(
1085 "ALTER TABLE {} ADD CONSTRAINT {} {};",
1086 quote_ident(&c.table_name),
1087 quote_ident(&c.name),
1088 c.definition
1089 ));
1090 }
1091 SchemaDiff::ConstraintDropped { table, name } => {
1092 statements.push(format!(
1093 "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {};",
1094 quote_ident(table),
1095 quote_ident(name)
1096 ));
1097 }
1098 SchemaDiff::TriggerAdded(t) => {
1099 statements.push(format!(
1100 "-- Trigger {} on {} needs manual creation",
1101 t.name, t.table_name
1102 ));
1103 }
1104 SchemaDiff::TriggerDropped { table, name } => {
1105 statements.push(format!(
1106 "DROP TRIGGER IF EXISTS {} ON {};",
1107 quote_ident(name),
1108 quote_ident(table)
1109 ));
1110 }
1111 SchemaDiff::ExtensionAdded(name) => {
1112 statements.push(format!(
1113 "CREATE EXTENSION IF NOT EXISTS {};",
1114 quote_ident(name)
1115 ));
1116 }
1117 SchemaDiff::ExtensionDropped(name) => {
1118 statements.push(format!("DROP EXTENSION IF EXISTS {};", quote_ident(name)));
1119 }
1120 }
1121 }
1122
1123 statements.join("\n\n")
1124}
1125
1126pub fn to_ddl(snapshot: &SchemaSnapshot) -> String {
1128 let mut statements = Vec::new();
1129
1130 for ext in &snapshot.extensions {
1132 statements.push(format!(
1133 "CREATE EXTENSION IF NOT EXISTS {};",
1134 quote_ident(ext)
1135 ));
1136 }
1137
1138 for e in &snapshot.enums {
1140 let values: Vec<String> = e
1141 .values
1142 .iter()
1143 .map(|v| crate::db::quote_literal(v))
1144 .collect();
1145 statements.push(format!(
1146 "CREATE TYPE {} AS ENUM ({});",
1147 quote_ident(&e.name),
1148 values.join(", ")
1149 ));
1150 }
1151
1152 for s in &snapshot.sequences {
1154 statements.push(format!("CREATE SEQUENCE {};", quote_ident(&s.name)));
1155 }
1156
1157 for t in &snapshot.tables {
1159 let cols: Vec<String> = t
1160 .columns
1161 .iter()
1162 .map(|c| {
1163 let mut col = format!(" {} {}", quote_ident(&c.name), c.data_type);
1164 if !c.is_nullable {
1165 col.push_str(" NOT NULL");
1166 }
1167 if let Some(ref default) = c.default {
1168 col.push_str(&format!(" DEFAULT {}", default));
1169 }
1170 col
1171 })
1172 .collect();
1173 statements.push(format!(
1174 "CREATE TABLE {} (\n{}\n);",
1175 quote_ident(&t.name),
1176 cols.join(",\n")
1177 ));
1178 }
1179
1180 for c in &snapshot.constraints {
1182 statements.push(format!(
1183 "ALTER TABLE {} ADD CONSTRAINT {} {};",
1184 quote_ident(&c.table_name),
1185 quote_ident(&c.name),
1186 c.definition
1187 ));
1188 }
1189
1190 for idx in &snapshot.indexes {
1192 statements.push(format!("{};", idx.definition));
1193 }
1194
1195 for v in &snapshot.views {
1197 let keyword = if v.is_materialized {
1198 "MATERIALIZED VIEW"
1199 } else {
1200 "VIEW"
1201 };
1202 statements.push(format!(
1203 "CREATE {} {} AS {};",
1204 keyword,
1205 quote_ident(&v.name),
1206 v.definition.trim_end_matches(';').trim()
1207 ));
1208 }
1209
1210 for func in &snapshot.functions {
1212 statements.push(format!("{};", func.definition.trim_end_matches(';')));
1213 }
1214
1215 for t in &snapshot.triggers {
1217 statements.push(format!(
1218 "-- Trigger {} on {}: {}",
1219 t.name, t.table_name, t.definition
1220 ));
1221 }
1222
1223 statements.join("\n\n")
1224}
1225
1226pub fn generate_ddl_mysql(diffs: &[SchemaDiff]) -> String {
1233 fn q(name: &str) -> String {
1234 format!("`{}`", name.replace('`', "``"))
1235 }
1236
1237 let dropped_tables: std::collections::HashSet<&str> = diffs
1242 .iter()
1243 .filter_map(|d| match d {
1244 SchemaDiff::TableDropped(name) => Some(name.as_str()),
1245 _ => None,
1246 })
1247 .collect();
1248 let references_dropped_table = |t: &str| dropped_tables.contains(t);
1249
1250 let mut statements = Vec::new();
1255 for d in diffs {
1256 match d {
1261 SchemaDiff::ColumnAdded { table, .. }
1262 | SchemaDiff::ColumnDropped { table, .. }
1263 | SchemaDiff::ColumnAltered { table, .. }
1264 | SchemaDiff::ConstraintDropped { table, .. }
1265 | SchemaDiff::TriggerDropped { table, .. }
1266 | SchemaDiff::IndexDropped {
1267 table_name: table, ..
1268 } if references_dropped_table(table) => continue,
1269 SchemaDiff::ConstraintAdded(c) if references_dropped_table(&c.table_name) => continue,
1270 SchemaDiff::TriggerAdded(t) if references_dropped_table(&t.table_name) => continue,
1271 SchemaDiff::IndexAdded(i) if references_dropped_table(&i.table_name) => continue,
1272 _ => {}
1273 }
1274 match d {
1275 SchemaDiff::TableAdded(t) => {
1276 let cols: Vec<String> = t
1277 .columns
1278 .iter()
1279 .map(|c| {
1280 let mut col = format!(" {} {}", q(&c.name), c.data_type);
1281 if !c.is_nullable {
1282 col.push_str(" NOT NULL");
1283 }
1284 if let Some(ref default) = c.default {
1285 col.push_str(&format!(" DEFAULT {}", default));
1286 }
1287 col
1288 })
1289 .collect();
1290 statements.push(format!(
1291 "CREATE TABLE {} (\n{}\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;",
1292 q(&t.name),
1293 cols.join(",\n")
1294 ));
1295 }
1296 SchemaDiff::TableDropped(name) => {
1297 statements.push(format!("DROP TABLE IF EXISTS {};", q(name)));
1298 }
1299 SchemaDiff::ColumnAdded { table, column } => {
1300 let mut stmt = format!(
1301 "ALTER TABLE {} ADD COLUMN {} {}",
1302 q(table),
1303 q(&column.name),
1304 column.data_type
1305 );
1306 if !column.is_nullable {
1307 stmt.push_str(" NOT NULL");
1308 }
1309 if let Some(ref default) = column.default {
1310 stmt.push_str(&format!(" DEFAULT {}", default));
1311 }
1312 stmt.push(';');
1313 statements.push(stmt);
1314 }
1315 SchemaDiff::ColumnDropped { table, column } => {
1316 statements.push(format!(
1317 "ALTER TABLE {} DROP COLUMN {};",
1318 q(table),
1319 q(column)
1320 ));
1321 }
1322 SchemaDiff::ColumnAltered {
1323 table, column, to, ..
1324 } => {
1325 let mut clause = format!(
1327 "ALTER TABLE {} MODIFY COLUMN {} {}",
1328 q(table),
1329 q(column),
1330 to.data_type
1331 );
1332 if !to.is_nullable {
1333 clause.push_str(" NOT NULL");
1334 }
1335 if let Some(ref default) = to.default {
1336 clause.push_str(&format!(" DEFAULT {}", default));
1337 }
1338 clause.push(';');
1339 statements.push(clause);
1340 }
1341 SchemaDiff::IndexAdded(idx) => {
1342 statements.push(format!("{};", idx.definition.trim_end_matches(';')));
1344 }
1345 SchemaDiff::IndexDropped { name, table_name } => {
1346 statements.push(format!("DROP INDEX {} ON {};", q(name), q(table_name)));
1348 }
1349 SchemaDiff::ViewAdded(v) => {
1350 statements.push(format!(
1351 "CREATE VIEW {} AS {};",
1352 q(&v.name),
1353 v.definition.trim_end_matches(';').trim()
1354 ));
1355 }
1356 SchemaDiff::ViewDropped(name) => {
1357 statements.push(format!("DROP VIEW IF EXISTS {};", q(name)));
1358 }
1359 SchemaDiff::ViewAltered { name, to, .. } => {
1360 statements.push(format!(
1361 "CREATE OR REPLACE VIEW {} AS {};",
1362 q(name),
1363 to.trim_end_matches(';').trim()
1364 ));
1365 }
1366 SchemaDiff::SequenceAdded(_) | SchemaDiff::SequenceDropped(_) => {
1367 statements.push("-- (sequence diff omitted: MySQL has no sequences)".into());
1369 }
1370 SchemaDiff::FunctionAdded(func) => {
1371 statements.push(format!("{};", func.definition.trim_end_matches(';')));
1372 }
1373 SchemaDiff::FunctionDropped(name) => {
1374 statements.push(format!("DROP FUNCTION IF EXISTS {};", q(name)));
1375 }
1376 SchemaDiff::FunctionAltered { name } => {
1377 statements.push(format!(
1378 "-- Function {} altered; manual review needed",
1379 name
1380 ));
1381 }
1382 SchemaDiff::EnumAdded(_) | SchemaDiff::EnumDropped(_) => {
1383 statements
1384 .push("-- (enum diff omitted: MySQL ENUM is a column-type modifier)".into());
1385 }
1386 SchemaDiff::ConstraintAdded(c) => {
1387 if c.definition.is_empty() {
1388 statements.push(format!(
1389 "-- ALTER TABLE {} ADD CONSTRAINT {} (definition unavailable)",
1390 q(&c.table_name),
1391 q(&c.name)
1392 ));
1393 } else {
1394 statements.push(format!(
1395 "ALTER TABLE {} ADD CONSTRAINT {} {};",
1396 q(&c.table_name),
1397 q(&c.name),
1398 c.definition
1399 ));
1400 }
1401 }
1402 SchemaDiff::ConstraintDropped { table, name } => {
1403 statements.push(format!(
1404 "ALTER TABLE {} DROP CONSTRAINT {};",
1405 q(table),
1406 q(name)
1407 ));
1408 }
1409 SchemaDiff::TriggerAdded(t) => {
1410 statements.push(format!(
1411 "-- Trigger {} on {}: {}",
1412 t.name, t.table_name, t.definition
1413 ));
1414 }
1415 SchemaDiff::TriggerDropped { table, name } => {
1416 statements.push(format!("DROP TRIGGER IF EXISTS {}.{};", q(table), q(name)));
1417 }
1418 SchemaDiff::ExtensionAdded(_) | SchemaDiff::ExtensionDropped(_) => {
1419 statements.push("-- (extension diff omitted: MySQL has no extensions)".into());
1420 }
1421 }
1422 }
1423 statements.join("\n\n")
1424}
1425
1426#[cfg(feature = "mysql")]
1434pub async fn introspect_mysql(client: &DbClient, schema: &str) -> Result<SchemaSnapshot> {
1435 use mysql_async::prelude::*;
1436 let pool = client.as_mysql()?;
1437 let mut conn = pool.get_conn().await?;
1438
1439 let column_rows: Vec<(String, String, String, String, Option<String>, i32)> = conn
1441 .exec(
1442 "SELECT t.TABLE_NAME, c.COLUMN_NAME, c.DATA_TYPE, c.IS_NULLABLE, \
1443 c.COLUMN_DEFAULT, c.ORDINAL_POSITION \
1444 FROM information_schema.TABLES t \
1445 JOIN information_schema.COLUMNS c \
1446 ON c.TABLE_SCHEMA = t.TABLE_SCHEMA AND c.TABLE_NAME = t.TABLE_NAME \
1447 WHERE t.TABLE_SCHEMA = ? AND t.TABLE_TYPE = 'BASE TABLE' \
1448 ORDER BY t.TABLE_NAME, c.ORDINAL_POSITION",
1449 (schema,),
1450 )
1451 .await?;
1452 let mut table_map: HashMap<String, Vec<ColumnDef>> = HashMap::new();
1453 for (table, col, dtype, nullable, default, ord) in column_rows {
1454 table_map.entry(table).or_default().push(ColumnDef {
1455 name: col,
1456 data_type: dtype,
1457 is_nullable: nullable == "YES",
1458 default,
1459 ordinal_position: ord,
1460 });
1461 }
1462 let mut tables: Vec<TableDef> = table_map
1463 .into_iter()
1464 .map(|(name, columns)| TableDef {
1465 schema: schema.to_string(),
1466 name,
1467 columns,
1468 })
1469 .collect();
1470 tables.sort_by(|a, b| a.name.cmp(&b.name));
1471
1472 let view_rows: Vec<(String, String)> = conn
1474 .exec(
1475 "SELECT TABLE_NAME, VIEW_DEFINITION FROM information_schema.VIEWS \
1476 WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME",
1477 (schema,),
1478 )
1479 .await?;
1480 let views: Vec<ViewDef> = view_rows
1481 .into_iter()
1482 .map(|(name, def)| ViewDef {
1483 schema: schema.to_string(),
1484 name,
1485 definition: def,
1486 is_materialized: false,
1487 })
1488 .collect();
1489
1490 let index_rows: Vec<(String, String, i32, String, i64)> = conn
1493 .exec(
1494 "SELECT TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX, COLUMN_NAME, NON_UNIQUE \
1495 FROM information_schema.STATISTICS \
1496 WHERE TABLE_SCHEMA = ? AND INDEX_NAME <> 'PRIMARY' \
1497 ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX",
1498 (schema,),
1499 )
1500 .await?;
1501 let mut idx_map: HashMap<(String, String), (Vec<String>, bool)> = HashMap::new();
1502 for (table, idx_name, _seq, col, non_unique) in index_rows {
1503 let entry = idx_map
1504 .entry((table, idx_name))
1505 .or_insert_with(|| (Vec::new(), non_unique == 0));
1506 entry.0.push(col);
1507 }
1508 let mut indexes: Vec<IndexDef> = idx_map
1509 .into_iter()
1510 .map(|((table, name), (cols, is_unique))| {
1511 let kw = if is_unique {
1512 "CREATE UNIQUE INDEX"
1513 } else {
1514 "CREATE INDEX"
1515 };
1516 let definition = format!(
1517 "{} `{}` ON `{}` ({})",
1518 kw,
1519 name,
1520 table,
1521 cols.iter()
1522 .map(|c| format!("`{}`", c))
1523 .collect::<Vec<_>>()
1524 .join(", ")
1525 );
1526 IndexDef {
1527 schema: schema.to_string(),
1528 name,
1529 table_name: table,
1530 definition,
1531 is_unique,
1532 }
1533 })
1534 .collect();
1535 indexes.sort_by(|a, b| a.name.cmp(&b.name));
1536
1537 let routine_rows: Vec<(String, String, String, String)> = conn
1539 .exec(
1540 "SELECT ROUTINE_NAME, \
1541 COALESCE(DTD_IDENTIFIER, ''), \
1542 COALESCE(EXTERNAL_LANGUAGE, ROUTINE_BODY), \
1543 COALESCE(ROUTINE_DEFINITION, '') \
1544 FROM information_schema.ROUTINES \
1545 WHERE ROUTINE_SCHEMA = ? ORDER BY ROUTINE_NAME",
1546 (schema,),
1547 )
1548 .await?;
1549 let functions: Vec<FunctionDef> = routine_rows
1550 .into_iter()
1551 .map(|(name, return_type, language, definition)| FunctionDef {
1552 schema: schema.to_string(),
1553 name,
1554 arguments: String::new(),
1555 return_type,
1556 language,
1557 definition,
1558 })
1559 .collect();
1560
1561 let constraint_rows: Vec<(String, String, String)> = conn
1564 .exec(
1565 "SELECT TABLE_NAME, CONSTRAINT_NAME, CONSTRAINT_TYPE \
1566 FROM information_schema.TABLE_CONSTRAINTS \
1567 WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME, CONSTRAINT_NAME",
1568 (schema,),
1569 )
1570 .await?;
1571 let constraints: Vec<ConstraintDef> = constraint_rows
1572 .into_iter()
1573 .map(|(table, name, ctype)| ConstraintDef {
1574 schema: schema.to_string(),
1575 table_name: table,
1576 name,
1577 constraint_type: ctype,
1578 definition: String::new(),
1579 })
1580 .collect();
1581
1582 let trigger_rows: Vec<(String, String, String)> = conn
1584 .exec(
1585 "SELECT EVENT_OBJECT_TABLE, TRIGGER_NAME, ACTION_STATEMENT \
1586 FROM information_schema.TRIGGERS \
1587 WHERE TRIGGER_SCHEMA = ? ORDER BY EVENT_OBJECT_TABLE, TRIGGER_NAME",
1588 (schema,),
1589 )
1590 .await?;
1591 let triggers: Vec<TriggerDef> = trigger_rows
1592 .into_iter()
1593 .map(|(table_name, name, definition)| TriggerDef {
1594 schema: schema.to_string(),
1595 table_name,
1596 name,
1597 definition,
1598 })
1599 .collect();
1600
1601 Ok(SchemaSnapshot {
1602 tables,
1603 views,
1604 indexes,
1605 sequences: Vec::new(),
1606 functions,
1607 enums: Vec::new(),
1608 constraints,
1609 triggers,
1610 extensions: Vec::new(),
1611 })
1612}
1613
1614#[cfg(test)]
1615mod tests_generate_ddl_mysql {
1616 use super::*;
1617
1618 fn col(name: &str, ty: &str) -> ColumnDef {
1619 ColumnDef {
1620 name: name.into(),
1621 data_type: ty.into(),
1622 is_nullable: false,
1623 default: None,
1624 ordinal_position: 1,
1625 }
1626 }
1627
1628 #[test]
1629 fn drop_index_emits_on_clause() {
1630 let diffs = vec![SchemaDiff::IndexDropped {
1631 name: "idx_users_email".into(),
1632 table_name: "users".into(),
1633 }];
1634 let sql = generate_ddl_mysql(&diffs);
1635 assert!(sql.contains("DROP INDEX `idx_users_email` ON `users`"));
1636 assert!(!sql.contains("DROP INDEX `idx_users_email`;"));
1638 }
1639
1640 #[test]
1641 fn dependent_diffs_filtered_when_parent_table_dropped() {
1642 let diffs = vec![
1646 SchemaDiff::ConstraintDropped {
1647 table: "t".into(),
1648 name: "PRIMARY".into(),
1649 },
1650 SchemaDiff::IndexDropped {
1651 name: "idx_t_x".into(),
1652 table_name: "t".into(),
1653 },
1654 SchemaDiff::ColumnDropped {
1655 table: "t".into(),
1656 column: "x".into(),
1657 },
1658 SchemaDiff::TableDropped("t".into()),
1659 ];
1660 let sql = generate_ddl_mysql(&diffs);
1661 assert!(sql.contains("DROP TABLE IF EXISTS `t`;"));
1662 assert!(!sql.contains("DROP CONSTRAINT"));
1663 assert!(!sql.contains("DROP INDEX `idx_t_x`"));
1664 assert!(!sql.contains("DROP COLUMN"));
1665 }
1666
1667 #[test]
1668 fn table_added_uses_innodb_utf8mb4() {
1669 let diffs = vec![SchemaDiff::TableAdded(TableDef {
1670 schema: "db".into(),
1671 name: "t".into(),
1672 columns: vec![col("id", "int")],
1673 })];
1674 let sql = generate_ddl_mysql(&diffs);
1675 assert!(sql.contains("CREATE TABLE `t`"));
1676 assert!(sql.contains("ENGINE=InnoDB"));
1677 assert!(sql.contains("utf8mb4"));
1678 }
1679}