kmp_adapter_embedded/adapter/
migration.rs1use std::fs;
10use std::path::Path;
11
12use kmp_domain::{ContextUpdatedEvent, PortError, ProjectionMutation};
13use serde::{Deserialize, Serialize};
14
15use super::engine::{Key, Table};
16use super::format_version::{self, StorageEngine};
17use super::store::EmbeddedKernelStore;
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct StoreMigrationReceipt {
22 pub source_format: u32,
23 pub source_sha256: String,
24 pub destination_format: u32,
25 pub events_migrated: u64,
26 pub mutations_applied: u64,
27 pub kernel_version: String,
28}
29
30impl StoreMigrationReceipt {
31 pub const MIGRATION_ID: &'static str = "store-format-migration";
33}
34
35impl EmbeddedKernelStore {
36 pub async fn migrate_data_dir<F>(
38 source_dir: &Path,
39 destination_dir: &Path,
40 derive: F,
41 ) -> Result<(Self, StoreMigrationReceipt), PortError>
42 where
43 F: Fn(&ContextUpdatedEvent) -> Result<Vec<ProjectionMutation>, PortError> + Send + 'static,
44 {
45 Self::migrate_data_dir_to(source_dir, destination_dir, StorageEngine::Sqlite, derive).await
46 }
47
48 pub async fn migrate_data_dir_to<F>(
50 source_dir: &Path,
51 destination_dir: &Path,
52 destination_engine: StorageEngine,
53 _derive: F,
54 ) -> Result<(Self, StoreMigrationReceipt), PortError>
55 where
56 F: Fn(&ContextUpdatedEvent) -> Result<Vec<ProjectionMutation>, PortError> + Send + 'static,
57 {
58 if same_file(source_dir, destination_dir) {
59 return Err(PortError::InvalidState(
60 "migration source and destination are the same data directory".to_string(),
61 ));
62 }
63 let source_format = format_version::read_stamped_version(source_dir)?;
64 if source_format > StorageEngine::NEWEST_KNOWN_FORMAT_VERSION {
65 return Err(PortError::InvalidState(format!(
66 "migration source `{}` uses format version {source_format}, newer than this \
67 binary supports ({}); upgrade the binary",
68 source_dir.display(),
69 StorageEngine::NEWEST_KNOWN_FORMAT_VERSION
70 )));
71 }
72 if source_format == StorageEngine::Sqlite.format_version() {
73 return Err(PortError::Unavailable(format!(
74 "migration from a SQLite format-3 store is unnecessary and unsupported; the \
75 source at `{}` is left untouched",
76 source_dir.display()
77 )));
78 }
79 let _ = destination_engine;
80 Err(PortError::InvalidState(format!(
81 "migration source `{}` uses unsupported format version {source_format}; current \
82 KMP left it untouched. Old contracts are not migrated by this redesign; \
83 use a compatible binary to inspect the source and start a fresh current store",
84 source_dir.display()
85 )))
86 }
87
88 pub async fn open_or_migrate_data_dir<F>(
90 source_dir: &Path,
91 destination_dir: &Path,
92 derive: F,
93 ) -> Result<(Self, Option<StoreMigrationReceipt>), PortError>
94 where
95 F: Fn(&ContextUpdatedEvent) -> Result<Vec<ProjectionMutation>, PortError> + Send + 'static,
96 {
97 Self::open_or_migrate_data_dir_to(
98 source_dir,
99 destination_dir,
100 StorageEngine::Sqlite,
101 derive,
102 )
103 .await
104 }
105
106 pub async fn open_or_migrate_data_dir_to<F>(
108 source_dir: &Path,
109 destination_dir: &Path,
110 destination_engine: StorageEngine,
111 derive: F,
112 ) -> Result<(Self, Option<StoreMigrationReceipt>), PortError>
113 where
114 F: Fn(&ContextUpdatedEvent) -> Result<Vec<ProjectionMutation>, PortError> + Send + 'static,
115 {
116 if format_version::existing_store_file(destination_dir).is_some() {
117 let store = Self::open(destination_dir)?;
118 let receipt = store.migration_receipt().await?;
119 return Ok((store, receipt));
120 }
121 let (store, receipt) =
122 Self::migrate_data_dir_to(source_dir, destination_dir, destination_engine, derive)
123 .await?;
124 Ok((store, Some(receipt)))
125 }
126
127 pub async fn migration_receipt(&self) -> Result<Option<StoreMigrationReceipt>, PortError> {
129 self.run(|store| {
130 let tx = store.begin_read()?;
131 let Some(raw) = tx.get(
132 Table::Migrations,
133 Key::Str(StoreMigrationReceipt::MIGRATION_ID),
134 )?
135 else {
136 return Ok(None);
137 };
138 let receipt = serde_json::from_slice(&raw).map_err(|error| {
139 PortError::InvalidState(format!("migration receipt is unreadable: {error}"))
140 })?;
141 Ok(Some(receipt))
142 })
143 .await
144 }
145}
146
147fn same_file(left: &Path, right: &Path) -> bool {
148 match (fs::canonicalize(left), fs::canonicalize(right)) {
149 (Ok(left), Ok(right)) => left == right,
150 _ => left == right,
151 }
152}