1use cratefield_core::Harness;
20
21#[cfg(feature = "postgres")]
22#[cfg(feature = "postgres")]
25const FLEET_PARALLELISM: usize = 8;
26
27pub fn apply(
38 harness: &Harness,
39 dialect: &str,
40 url: &str,
41 plan: bool,
42 fleet: bool,
43 tenant: Option<&str>,
44 strict: bool,
45) -> Result<(), String> {
46 if dialect != "postgres" {
47 return Err(format!(
48 "dialect {dialect:?} is not supported for apply (only postgres, issue #18)"
49 ));
50 }
51 if plan && fleet {
52 return Err(
53 "--plan and --fleet contradict each other: --plan applies nothing, --fleet \
54 applies to every registered tenant"
55 .to_owned(),
56 );
57 }
58 if tenant.is_some() && !plan {
59 return Err(
60 "--tenant only means something with --plan today: fleet apply is the runtime \
61 reconciler's job, and one tenant cannot be applied without its neighbours' \
62 registry being read anyway"
63 .to_owned(),
64 );
65 }
66 if strict && !fleet {
67 return Err(
68 "--strict says what to do about a degraded tenant, and only --fleet has \
69 tenants: a plain apply either applies its migrations or fails"
70 .to_owned(),
71 );
72 }
73 #[cfg(not(feature = "postgres"))]
74 {
75 let _ = (harness, url, plan, fleet, tenant, strict);
76 Err(
77 "this fz binary was built without cratefield-cli's `postgres` feature — \
78 rebuild the fz bin with `--features cratefield-cli/postgres` to apply \
79 migrations to Postgres (sqlite/D1 migrations run through \
80 `wrangler d1 migrations apply`)"
81 .to_owned(),
82 )
83 }
84 #[cfg(feature = "postgres")]
85 {
86 if plan {
87 apply_plan(harness, url, tenant)
88 } else if fleet {
89 apply_fleet(harness, url, strict)
90 } else {
91 apply_postgres(harness, url)
92 }
93 }
94}
95
96#[cfg(feature = "postgres")]
97fn runtime() -> Result<tokio::runtime::Runtime, String> {
98 tokio::runtime::Builder::new_current_thread()
99 .enable_all()
100 .build()
101 .map_err(|err| format!("cannot start the async runtime: {err}"))
102}
103
104#[cfg(feature = "postgres")]
107fn apply_postgres(harness: &Harness, url: &str) -> Result<(), String> {
108 runtime()?.block_on(async {
109 let db = cratefield_adapter_postgres::Postgres::connect(url)
111 .await
112 .map_err(|err| format!("cannot connect (check --url): {err}"))?;
113 db.apply_harness_migrations(harness)
114 .await
115 .map_err(|err| format!("migrations failed: {err}"))
116 })
117}
118
119#[cfg(feature = "postgres")]
123fn apply_fleet(harness: &Harness, url: &str, strict: bool) -> Result<(), String> {
124 runtime()?.block_on(async {
125 let db = cratefield_adapter_postgres::Postgres::connect(url)
126 .await
127 .map_err(|err| {
128 format!("cannot connect to the control database (check --url): {err}")
129 })?;
130 let reports = db
131 .reconcile_fleet(harness, FLEET_PARALLELISM)
132 .await
133 .map_err(|err| format!("reconciliation failed: {err}"))?;
134 if reports.is_empty() {
137 println!(
138 "no tenants registered in the control database — nothing to reconcile \
139 (a venture database is applied without --fleet)"
140 );
141 return Ok(());
142 }
143 let degraded: Vec<&cratefield_adapter_postgres::TenantReport> = reports
144 .iter()
145 .filter(|report| report.status == cratefield_adapter_postgres::TenantStatus::Degraded)
146 .collect();
147 for report in &reports {
148 println!(
149 "tenant {}: {} applied, {} skipped, {}{}",
150 report.tenant,
151 report.applied,
152 report.skipped,
153 report.status,
154 report
155 .error
156 .as_deref()
157 .map(|err| format!(" — {err}"))
158 .unwrap_or_default()
159 );
160 }
161 if degraded.is_empty() {
162 Ok(())
163 } else if strict {
164 Err(format!(
165 "{} tenant(s) degraded ({}): boot would abort in strict mode",
166 degraded.len(),
167 degraded
168 .iter()
169 .map(|report| report.tenant.as_str())
170 .collect::<Vec<_>>()
171 .join(", ")
172 ))
173 } else {
174 println!(
175 "{} tenant(s) degraded; every other tenant serves (re-run with --strict \
176 to make this a failure)",
177 degraded.len()
178 );
179 Ok(())
180 }
181 })
182}
183
184#[cfg(feature = "postgres")]
188fn apply_plan(harness: &Harness, url: &str, tenant: Option<&str>) -> Result<(), String> {
189 runtime()?.block_on(async {
190 let control = cratefield_adapter_postgres::Postgres::connect(url)
191 .await
192 .map_err(|err| {
193 format!("cannot connect to the control database (check --url): {err}")
194 })?;
195 control
196 .bootstrap_registry()
197 .await
198 .map_err(|err| format!("cannot bootstrap the registry: {err}"))?;
199 let records = control
200 .tenants()
201 .await
202 .map_err(|err| format!("cannot read the tenant registry: {err}"))?;
203 let records: Vec<_> = match tenant {
204 Some(name) => {
205 let found: Vec<_> = records
206 .iter()
207 .filter(|record| record.tenant == name)
208 .collect();
209 if found.is_empty() {
210 return Err(format!("tenant {name:?} is not in the registry"));
211 }
212 found
213 }
214 None => records.iter().collect(),
215 };
216 if records.is_empty() {
217 println!("no tenants registered");
218 return Ok(());
219 }
220 for record in records {
221 match control.reconcile_plan(record, harness).await {
222 Ok(plan) => {
223 if plan.modules.is_empty() {
224 println!("tenant {}: nothing to apply", plan.tenant);
225 } else {
226 println!("tenant {}:", plan.tenant);
227 for module in &plan.modules {
228 println!(" {}: {}", module.module, module.migrations.join(", "));
229 }
230 }
231 }
232 Err(err) => println!("tenant {}: unreachable — {err}", record.tenant),
233 }
234 }
235 Ok(())
236 })
237}