tauri_store/
migration.rs

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
105/// A migration step.
106pub struct Migration {
107  inner: Box<MigrationFn>,
108  version: Version,
109}
110
111impl Migration {
112  /// Creates a new migration.
113  ///
114  /// # Panics
115  ///
116  /// Panics if the version is not a valid [semver](https://semver.org/).
117  #[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  /// Version of the migration.
129  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/// Context for a migration step.
155#[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// This way the meta can be updated even if some migrations fail.
178#[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}