diff_as_lib/
diff_as_lib.rs1use std::collections::BTreeMap;
16
17use anyhow::Result;
18use diffly::{
19 presentation::writers::{all_writers, write_to_file, writer_for},
20 AppConfig, Changeset, DbConfig, DiffConfig, ExcludedColumns, OutputConfig, TableConfig,
21};
22
23#[tokio::main]
24async fn main() -> Result<()> {
25 let args: Vec<String> = std::env::args().collect();
26
27 match args.get(1).map(String::as_str) {
28 Some(path) => from_config_file(path).await,
29 None => programmatic_config().await,
30 }
31}
32
33async fn from_config_file(path: &str) -> Result<()> {
37 println!("=== Pattern 1: from config file ({path}) ===\n");
38
39 let cfg = AppConfig::load(path)?;
40 let changeset = diffly::run(&cfg).await?;
41
42 for writer in all_writers() {
44 write_to_file(&*writer, &changeset, &cfg.output.dir)?;
45 println!(
46 "Written: {}/{}.{}",
47 cfg.output.dir,
48 changeset.changeset_id,
49 writer.extension()
50 );
51 }
52
53 print_summary(&changeset);
54 Ok(())
55}
56
57async fn programmatic_config() -> Result<()> {
62 println!("=== Pattern 2: programmatic config ===\n");
63
64 let db = |schema: &str| DbConfig {
65 driver: "postgres".into(),
66 host: std::env::var("DB_HOST").unwrap_or_else(|_| "localhost".into()),
67 port: 5432,
68 dbname: "diffly".into(),
69 user: "diffly".into(),
70 password: "diffly".into(),
71 schema: schema.into(),
72 };
73
74 let cfg = AppConfig {
75 source: db("source"),
76 target: db("target"),
77 diff: DiffConfig {
78 tables: vec![
79 TableConfig {
80 name: "pricing_rules".into(),
81 primary_key: vec!["id".into()],
82 excluded_columns: ExcludedColumns(vec![
83 "created_at".into(),
84 "updated_at".into(),
85 ]),
86 },
87 TableConfig {
88 name: "discount_tiers".into(),
89 primary_key: vec!["id".into()],
90 excluded_columns: ExcludedColumns::default(),
91 },
92 TableConfig {
93 name: "tax_rules".into(),
94 primary_key: vec!["region_code".into(), "product_category".into()],
95 excluded_columns: ExcludedColumns::default(),
96 },
97 ],
98 },
99 output: OutputConfig {
100 dir: "./output".into(),
101 },
102 };
103
104 let changeset = diffly::run(&cfg).await?;
105
106 let sql_writer = writer_for("sql").expect("sql writer always available");
108 write_to_file(&*sql_writer, &changeset, &cfg.output.dir)?;
109 println!(
110 "SQL written: {}/{}.sql\n",
111 cfg.output.dir, changeset.changeset_id
112 );
113
114 inspect_changeset(&changeset);
116 Ok(())
117}
118
119fn inspect_changeset(changeset: &Changeset) {
124 println!("=== Pattern 3: inspecting the changeset ===\n");
125 println!("id : {}", changeset.changeset_id);
126 println!("source : {}", changeset.source_schema);
127 println!("target : {}", changeset.target_schema);
128 println!("driver : {}", changeset.driver);
129 println!();
130
131 for table in &changeset.tables {
132 if table.is_empty() {
133 continue;
134 }
135
136 println!("━━ {} ━━", table.table_name);
137
138 for ins in &table.inserts {
139 println!(" + INSERT {}", fmt_pk(&ins.pk));
140 }
141
142 for upd in &table.updates {
143 let pk = fmt_pk(&upd.pk);
144 for col in &upd.changed_columns {
145 println!(
146 " ~ UPDATE {} {}: {} → {}",
147 pk, col.column, col.before, col.after
148 );
149 }
150 }
151
152 for del in &table.deletes {
153 println!(" - DELETE {}", fmt_pk(&del.pk));
154 }
155
156 println!();
157 }
158
159 if changeset.summary.total_deletes > 0 {
161 eprintln!(
162 "⚠ {} delete(s) detected — review before applying to production.",
163 changeset.summary.total_deletes,
164 );
165 }
166
167 let json = serde_json::to_string_pretty(changeset).expect("Changeset is always serialisable");
169 println!("Full changeset: {} bytes of JSON", json.len());
170
171 print_summary(changeset);
172}
173
174fn fmt_pk(pk: &BTreeMap<String, serde_json::Value>) -> String {
175 pk.iter()
176 .map(|(k, v)| format!("{k}={v}"))
177 .collect::<Vec<_>>()
178 .join(", ")
179}
180
181fn print_summary(changeset: &Changeset) {
182 println!("\n── summary ──────────────────────");
183 println!(" inserts : {}", changeset.summary.total_inserts);
184 println!(" updates : {}", changeset.summary.total_updates);
185 println!(" deletes : {}", changeset.summary.total_deletes);
186 println!(" tables : {}", changeset.summary.tables_affected);
187}