kmp_adapter_embedded/adapter/
migration.rs1use std::fs;
30use std::path::{Path, PathBuf};
31
32use kmp_domain::{ContextUpdatedEvent, PortError, ProjectionMutation};
33use serde::{Deserialize, Serialize};
34use sha2::{Digest, Sha256};
35
36use super::engine::{Key, Table};
37use super::format_version::{self, StorageEngine};
38use super::store::EmbeddedKernelStore;
39
40const SOURCE_COPY_FILE: &str = "migration-source.redb";
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct StoreMigrationReceipt {
47 pub source_format: u32,
48 pub source_sha256: String,
49 pub destination_format: u32,
50 pub events_migrated: u64,
51 pub mutations_applied: u64,
52 pub kernel_version: String,
53}
54
55impl StoreMigrationReceipt {
56 pub const MIGRATION_ID: &'static str = "store-format-migration";
58}
59
60impl EmbeddedKernelStore {
61 pub async fn migrate_data_dir<F>(
68 source_dir: &Path,
69 destination_dir: &Path,
70 derive: F,
71 ) -> Result<(Self, StoreMigrationReceipt), PortError>
72 where
73 F: Fn(&ContextUpdatedEvent) -> Result<Vec<ProjectionMutation>, PortError> + Send + 'static,
74 {
75 Self::migrate_data_dir_to(source_dir, destination_dir, StorageEngine::Sqlite, derive).await
76 }
77
78 pub async fn migrate_data_dir_to<F>(
86 source_dir: &Path,
87 destination_dir: &Path,
88 destination_engine: StorageEngine,
89 derive: F,
90 ) -> Result<(Self, StoreMigrationReceipt), PortError>
91 where
92 F: Fn(&ContextUpdatedEvent) -> Result<Vec<ProjectionMutation>, PortError> + Send + 'static,
93 {
94 if same_file(source_dir, destination_dir) {
95 return Err(PortError::InvalidState(
96 "migration source and destination are the same data directory".to_string(),
97 ));
98 }
99 let source_format = format_version::read_stamped_version(source_dir)?;
100 if source_format > StorageEngine::NEWEST_KNOWN_FORMAT_VERSION {
101 return Err(PortError::InvalidState(format!(
102 "migration source `{}` uses format version {source_format}, newer than this \
103 binary supports ({}); upgrade the binary",
104 source_dir.display(),
105 StorageEngine::NEWEST_KNOWN_FORMAT_VERSION
106 )));
107 }
108 let source_engine =
111 StorageEngine::from_format_version(source_format).unwrap_or(StorageEngine::Redb);
112 if source_engine != StorageEngine::Redb {
118 return Err(PortError::Unavailable(format!(
119 "migration from a {source_engine} store is not supported yet; the source at `{}` \
120 is left untouched",
121 source_dir.display()
122 )));
123 }
124 let source_store_file = format_version::store_file_path_for(source_dir, source_engine);
125 if !source_store_file.exists() {
126 return Err(PortError::InvalidState(format!(
127 "migration source `{}` holds no store file at `{}`",
128 source_dir.display(),
129 source_store_file.display()
130 )));
131 }
132 let source_sha256 = sha256_of(&source_store_file)?;
133
134 if format_version::existing_store_file(destination_dir).is_some() {
135 let already = match Self::open(destination_dir) {
139 Ok(store) => store.migration_receipt().await.ok().flatten(),
140 Err(_) => None,
141 };
142 if let Some(receipt) = already
143 && receipt.source_sha256 == source_sha256
144 {
145 return Err(PortError::Conflict(format!(
146 "migration destination `{}` was already migrated from this exact \
147 source ({} events, source sha256 {}); nothing to do",
148 destination_dir.display(),
149 receipt.events_migrated,
150 receipt.source_sha256
151 )));
152 }
153 return Err(PortError::Conflict(format!(
154 "migration destination `{}` already holds a store; migrate into a new \
155 directory rather than over existing memory",
156 destination_dir.display()
157 )));
158 }
159 let events = read_source_events(&source_store_file, source_engine, destination_dir)?;
160
161 let destination = Self::open_with_engine(destination_dir, destination_engine)?;
162 let events_migrated = destination.replay_event_stream(events).await?;
163 let rebuild = destination.rebuild_projections(derive).await?;
164
165 let source_sha256_after = sha256_of(&source_store_file)?;
169 if source_sha256_after != source_sha256 {
170 return Err(PortError::InvalidState(format!(
171 "migration modified its source `{}`; refusing to report success",
172 source_store_file.display()
173 )));
174 }
175
176 let receipt = StoreMigrationReceipt {
177 source_format,
178 source_sha256,
179 destination_format: destination_engine.format_version(),
180 events_migrated,
181 mutations_applied: rebuild.mutations_applied,
182 kernel_version: env!("CARGO_PKG_VERSION").to_string(),
183 };
184 destination.write_migration_receipt(&receipt).await?;
185 Ok((destination, receipt))
186 }
187
188 pub async fn open_or_migrate_data_dir<F>(
194 source_dir: &Path,
195 destination_dir: &Path,
196 derive: F,
197 ) -> Result<(Self, Option<StoreMigrationReceipt>), PortError>
198 where
199 F: Fn(&ContextUpdatedEvent) -> Result<Vec<ProjectionMutation>, PortError> + Send + 'static,
200 {
201 Self::open_or_migrate_data_dir_to(
202 source_dir,
203 destination_dir,
204 StorageEngine::Sqlite,
205 derive,
206 )
207 .await
208 }
209
210 pub async fn open_or_migrate_data_dir_to<F>(
215 source_dir: &Path,
216 destination_dir: &Path,
217 destination_engine: StorageEngine,
218 derive: F,
219 ) -> Result<(Self, Option<StoreMigrationReceipt>), PortError>
220 where
221 F: Fn(&ContextUpdatedEvent) -> Result<Vec<ProjectionMutation>, PortError> + Send + 'static,
222 {
223 if format_version::existing_store_file(destination_dir).is_some() {
224 let store = Self::open(destination_dir)?;
225 let receipt = store.migration_receipt().await?;
226 return Ok((store, receipt));
227 }
228 let (store, receipt) =
229 Self::migrate_data_dir_to(source_dir, destination_dir, destination_engine, derive)
230 .await?;
231 Ok((store, Some(receipt)))
232 }
233
234 pub async fn migration_receipt(&self) -> Result<Option<StoreMigrationReceipt>, PortError> {
236 self.run(|store| {
237 let tx = store.begin_read()?;
238 let Some(raw) = tx.get(
241 Table::Migrations,
242 Key::Str(StoreMigrationReceipt::MIGRATION_ID),
243 )?
244 else {
245 return Ok(None);
246 };
247 let receipt = serde_json::from_slice(&raw).map_err(|error| {
248 PortError::InvalidState(format!("migration receipt is unreadable: {error}"))
249 })?;
250 Ok(Some(receipt))
251 })
252 .await
253 }
254
255 async fn write_migration_receipt(
256 &self,
257 receipt: &StoreMigrationReceipt,
258 ) -> Result<(), PortError> {
259 let encoded = serde_json::to_vec(receipt).map_err(|error| {
260 PortError::InvalidState(format!("migration receipt is not encodable: {error}"))
261 })?;
262 self.run(move |store| {
263 let mut tx = store.begin_write()?;
264 tx.insert(
265 Table::Migrations,
266 Key::Str(StoreMigrationReceipt::MIGRATION_ID),
267 &encoded,
268 )?;
269 tx.commit()
270 })
271 .await
272 }
273}
274
275fn read_source_events(
281 source_store_file: &Path,
282 source_engine: StorageEngine,
283 destination_dir: &Path,
284) -> Result<Vec<ContextUpdatedEvent>, PortError> {
285 fs::create_dir_all(destination_dir).map_err(|error| {
286 PortError::Unavailable(format!(
287 "migration could not create destination `{}`: {error}",
288 destination_dir.display()
289 ))
290 })?;
291 let copy_path: PathBuf = destination_dir.join(SOURCE_COPY_FILE);
292 fs::copy(source_store_file, ©_path).map_err(|error| {
293 PortError::Unavailable(format!(
294 "migration could not copy the source store to `{}`: {error}",
295 copy_path.display()
296 ))
297 })?;
298
299 let events = {
300 let source = EmbeddedKernelStore::open_store_file(©_path, source_engine)?;
301 source.read_event_log_blocking()
302 };
303
304 let _ = fs::remove_file(©_path);
307 events
308}
309
310fn sha256_of(path: &Path) -> Result<String, PortError> {
311 let bytes = fs::read(path).map_err(|error| {
312 PortError::Unavailable(format!(
313 "migration could not read `{}`: {error}",
314 path.display()
315 ))
316 })?;
317 let mut hasher = Sha256::new();
318 hasher.update(&bytes);
319 Ok(format!("{:x}", hasher.finalize()))
320}
321
322fn same_file(left: &Path, right: &Path) -> bool {
323 match (fs::canonicalize(left), fs::canonicalize(right)) {
324 (Ok(left), Ok(right)) => left == right,
325 _ => left == right,
328 }
329}