siderite_migrations/executor/
intents.rs1use super::catalog::table_exists;
4use super::recovery::{read_index, text};
5use super::{Db, Migration, MigrationError as Error, MigrationRegistry};
6use super::{Value, placeholder, quote_star};
7use serde::Serialize;
8
9const TABLE: &str = "siderite_migration_intents";
10type Result<T> = std::result::Result<T, Error>;
11type Intents = Vec<MigrationIntent>;
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
15pub struct MigrationIntent {
16 pub migration_id: String,
18 pub checksum: String,
20 pub direction: String,
22 pub operation: usize,
24 pub callback: Option<String>,
26}
27
28impl MigrationIntent {
29 fn uncertain(&self) -> Error {
30 match &self.callback {
31 Some(callback) => Error::UncertainRustStep {
32 id: self.migration_id.clone(),
33 callback: callback.clone(),
34 operation: self.operation,
35 },
36 None => Error::UncertainSqlStep {
37 id: self.migration_id.clone(),
38 operation: self.operation,
39 },
40 }
41 }
42}
43
44pub(super) async fn read(db: &Db, id: Option<&str>) -> Result<Intents> {
53 if !table_exists(db, TABLE).await? {
54 return Ok(Vec::new());
55 }
56 let kind = db.capabilities().kind;
57 let mut sql = format!("SELECT * FROM {}", quote_star(kind, TABLE));
58 let mut params = Vec::new();
59 if let Some(id) = id {
60 let bind = placeholder(kind, 1);
61 sql.push_str(&format!(" WHERE migration_id = {bind}"));
62 params.push(Value::Text(id.to_owned()));
63 }
64 sql.push_str(" ORDER BY migration_id");
65 let mut intents = Vec::new();
66 for row in db.raw_sql(&sql, params).await?.rows {
67 let direction = text(&row, "direction")?;
68 if !matches!(direction.as_str(), "apply" | "unapply") {
69 return Err(Error::state("invalid intent direction"));
70 }
71 intents.push(MigrationIntent {
72 migration_id: text(&row, "migration_id")?,
73 checksum: text(&row, "checksum")?,
74 direction,
75 operation: read_index(&row, "operation_index")?,
76 callback: match row.get("callback") {
77 Some(Value::Null) => None,
78 _ => Some(text(&row, "callback")?),
79 },
80 });
81 }
82 Ok(intents)
83}
84
85pub(super) async fn guard(
96 db: &Db,
97 migration: &Migration,
98 registry: &MigrationRegistry,
99 direction: &str,
100) -> Result<()> {
101 for intent in read(db, Some(&migration.id)).await? {
102 if intent.checksum != migration.checksum {
103 return Err(Error::state(format!(
104 "migration `{}` changed since a partial execution; \
105 restore the original file before reconciliation",
106 migration.id,
107 )));
108 }
109 if intent.direction != direction
110 || !intent
111 .callback
112 .as_deref()
113 .is_some_and(|name| registry.is_replay_safe(name))
114 {
115 return Err(intent.uncertain());
116 }
117 }
118 Ok(())
119}
120
121pub(super) async fn begin(
133 db: &Db,
134 migration: &Migration,
135 direction: &str,
136 operation: usize,
137 callback: Option<&str>,
138) -> Result<()> {
139 let kind = db.capabilities().kind;
140 let table = quote_star(kind, TABLE);
141 db.execute_script(&format!(
142 "CREATE TABLE IF NOT EXISTS {table} (\
143 migration_id VARCHAR(255) PRIMARY KEY, \
144 checksum VARCHAR(255) NOT NULL, \
145 direction VARCHAR(16) NOT NULL, \
146 operation_index INTEGER NOT NULL, callback TEXT)"
147 ))
148 .await?;
149 if let Some(intent) = read(db, Some(&migration.id)).await?.first() {
150 let same_callback = intent.callback.as_deref() == callback;
151 if intent.operation != operation || !same_callback {
152 return Err(intent.uncertain());
153 }
154 return Ok(());
155 }
156 let values = (1..=5)
157 .map(|index| placeholder(kind, index))
158 .collect::<Vec<_>>()
159 .join(", ");
160 let operation = i64::try_from(operation).map_err(|_| index_overflow())?;
161 db.raw_execute(
162 &format!(
163 "INSERT INTO {table} (migration_id, checksum, direction, \
164 operation_index, callback) VALUES ({values})"
165 ),
166 vec![
167 Value::Text(migration.id.clone()),
168 Value::Text(migration.checksum.clone()),
169 Value::Text(direction.to_owned()),
170 Value::Int(operation),
171 callback.map_or(Value::Null, |name| Value::Text(name.to_owned())),
172 ],
173 )
174 .await?;
175 Ok(())
176}
177
178pub(super) async fn finish(db: &Db, migration: &Migration) -> Result<()> {
187 let kind = db.capabilities().kind;
188 let affected = db
189 .raw_execute(
190 &format!(
191 "DELETE FROM {} WHERE migration_id = {} AND checksum = {}",
192 quote_star(kind, TABLE),
193 placeholder(kind, 1),
194 placeholder(kind, 2),
195 ),
196 vec![
197 Value::Text(migration.id.clone()),
198 Value::Text(migration.checksum.clone()),
199 ],
200 )
201 .await?;
202 if affected != 1 {
203 return Err(Error::state("migration intent lost before completion"));
204 }
205 Ok(())
206}
207
208fn index_overflow() -> Error {
209 Error::state("migration operation index overflow")
210}
211
212pub(super) async fn finish_recorded_callback(
222 db: &Db,
223 migration: &Migration,
224 completed: usize,
225) -> Result<()> {
226 if completed == 0 {
227 return Ok(());
228 }
229 for intent in read(db, Some(&migration.id)).await? {
230 if intent.callback.is_some() && intent.operation < completed {
231 finish(db, migration).await?;
232 }
233 }
234 Ok(())
235}