1use crate::error::{Error, Result};
2use crate::store::{StoreId, StoreState};
3use itertools::Itertools;
4use semver::Version;
5use serde::{Deserialize, Serialize};
6use std::cmp::Ordering;
7use std::collections::HashMap;
8use tauri_store_utils::Semver;
9
10#[cfg(tauri_store_tracing)]
11use tracing::debug;
12
13type MigrationFn = dyn Fn(&mut StoreState) -> Result<()> + Send + Sync;
14type BeforeEachMigrationFn = dyn Fn(MigrationContext) + Send + Sync;
15
16#[doc(hidden)]
17#[derive(Default)]
18pub struct Migrator {
19 migrations: HashMap<StoreId, Vec<Migration>>,
20 before_each: Option<Box<BeforeEachMigrationFn>>,
21 pub(crate) history: MigrationHistory,
22}
23
24impl Migrator {
25 pub fn add_migration(&mut self, id: StoreId, migration: Migration) {
26 self
27 .migrations
28 .entry(id)
29 .or_default()
30 .push(migration);
31 }
32
33 pub fn add_migrations<I>(&mut self, id: StoreId, migrations: I)
34 where
35 I: IntoIterator<Item = Migration>,
36 {
37 self
38 .migrations
39 .entry(id)
40 .or_default()
41 .extend(migrations);
42 }
43
44 pub fn migrate(&mut self, id: &StoreId, state: &mut StoreState) -> MigrationResult {
45 let mut migrations = self
46 .migrations
47 .get(id)
48 .map(Vec::as_slice)
49 .unwrap_or_default()
50 .iter()
51 .sorted()
52 .collect_vec();
53
54 if let Some(last) = self.history.get(id) {
55 migrations.retain(|migration| migration.version > *last);
56 }
57
58 #[cfg(tauri_store_tracing)]
59 debug!("{} pending migration(s) for {}", migrations.len(), id);
60
61 if migrations.is_empty() {
62 return MigrationResult::new(0);
63 }
64
65 let mut iter = migrations.iter().peekable();
66 let mut previous = None;
67 let mut done = 0;
68
69 while let Some(migration) = iter.next() {
70 let current = &migration.version;
71 if let Some(before_each) = &self.before_each {
72 let next = iter.peek().map(|it| &it.version);
73 let context = MigrationContext { id, state, current, previous, next };
74
75 #[cfg(tauri_store_tracing)]
76 debug!(before_each_migration = ?context);
77
78 before_each(context);
79 }
80
81 if let Err(err) = (migration.inner)(state) {
82 return MigrationResult::with_error(done, err);
83 }
84
85 self.history.set(id, current);
86 previous = Some(current);
87 done += 1;
88
89 #[cfg(tauri_store_tracing)]
90 debug!("migration {current} done for {id}");
91 }
92
93 MigrationResult::new(done)
94 }
95
96 #[doc(hidden)]
97 pub fn on_before_each<F>(&mut self, f: F)
98 where
99 F: Fn(MigrationContext) + Send + Sync + 'static,
100 {
101 self.before_each = Some(Box::new(f));
102 }
103}
104
105pub struct Migration {
107 inner: Box<MigrationFn>,
108 version: Version,
109}
110
111impl Migration {
112 #[allow(clippy::needless_pass_by_value)]
118 pub fn new<F>(version: impl Semver, up: F) -> Self
119 where
120 F: Fn(&mut StoreState) -> Result<()> + Send + Sync + 'static,
121 {
122 Self {
123 inner: Box::new(up),
124 version: version.semver(),
125 }
126 }
127
128 pub fn version(&self) -> &Version {
130 &self.version
131 }
132}
133
134impl PartialOrd for Migration {
135 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
136 Some(self.cmp(other))
137 }
138}
139
140impl Ord for Migration {
141 fn cmp(&self, other: &Self) -> Ordering {
142 self.version.cmp(&other.version)
143 }
144}
145
146impl PartialEq for Migration {
147 fn eq(&self, other: &Self) -> bool {
148 self.version == other.version
149 }
150}
151
152impl Eq for Migration {}
153
154#[derive(Debug)]
156pub struct MigrationContext<'a> {
157 pub id: &'a StoreId,
158 pub state: &'a StoreState,
159 pub current: &'a Version,
160 pub previous: Option<&'a Version>,
161 pub next: Option<&'a Version>,
162}
163
164#[derive(Clone, Debug, Default, Serialize, Deserialize)]
165pub(crate) struct MigrationHistory(HashMap<StoreId, Version>);
166
167impl MigrationHistory {
168 pub fn get(&self, id: &StoreId) -> Option<&Version> {
169 self.0.get(id)
170 }
171
172 pub fn set(&mut self, id: &StoreId, version: &Version) {
173 self.0.insert(id.clone(), version.clone());
174 }
175}
176
177#[doc(hidden)]
179pub struct MigrationResult {
180 pub(crate) done: u32,
181 pub(crate) error: Option<Error>,
182}
183
184impl MigrationResult {
185 pub const fn new(done: u32) -> Self {
186 Self { done, error: None }
187 }
188
189 pub const fn with_error(done: u32, error: Error) -> Self {
190 Self { done, error: Some(error) }
191 }
192}