redb_extras/dbcopy/
mod.rs

1//! Database copy utilities for redb.
2//!
3//! This module provides helpers to copy data between databases using
4//! explicit table definitions supplied by callers.
5
6use crate::Result;
7use redb::{
8    Database, MultimapTableDefinition, MultimapTableHandle, ReadTransaction, ReadableDatabase,
9    ReadableMultimapTable, ReadableTable, TableDefinition, TableError, TableHandle,
10    WriteTransaction,
11};
12use std::fmt;
13use std::marker::PhantomData;
14
15#[cfg(test)]
16mod tests;
17
18/// Errors returned by database copy operations.
19#[derive(Debug)]
20pub enum DbCopyError {
21    /// One or more destination tables already exist.
22    DestinationTablesExist(Vec<String>),
23
24    /// Failed to check destination tables.
25    DestinationCheckFailed(String),
26
27    /// Failed to open a source table.
28    SourceTableOpenFailed(String),
29
30    /// Failed to open a destination table.
31    DestinationTableOpenFailed(String),
32
33    /// Failed while copying table contents.
34    TableCopyFailed(String),
35
36    /// Transaction failures during copy.
37    TransactionFailed(String),
38
39    /// Failed to commit the destination transaction.
40    CommitFailed(String),
41}
42
43impl std::error::Error for DbCopyError {}
44
45impl fmt::Display for DbCopyError {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        match self {
48            DbCopyError::DestinationTablesExist(names) => {
49                write!(f, "Destination already contains: {}", names.join(", "))
50            }
51            DbCopyError::DestinationCheckFailed(msg) => {
52                write!(f, "Destination check failed: {}", msg)
53            }
54            DbCopyError::SourceTableOpenFailed(msg) => {
55                write!(f, "Source table open failed: {}", msg)
56            }
57            DbCopyError::DestinationTableOpenFailed(msg) => {
58                write!(f, "Destination table open failed: {}", msg)
59            }
60            DbCopyError::TableCopyFailed(msg) => write!(f, "Table copy failed: {}", msg),
61            DbCopyError::TransactionFailed(msg) => write!(f, "Transaction failed: {}", msg),
62            DbCopyError::CommitFailed(msg) => write!(f, "Commit failed: {}", msg),
63        }
64    }
65}
66
67enum CopyKind {
68    Table,
69    Multimap,
70}
71
72impl fmt::Display for CopyKind {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        match self {
75            CopyKind::Table => write!(f, "table"),
76            CopyKind::Multimap => write!(f, "multimap table"),
77        }
78    }
79}
80
81trait CopyStep {
82    fn name(&self) -> &str;
83    fn kind(&self) -> CopyKind;
84    fn preflight(&self, destination: &ReadTransaction) -> std::result::Result<bool, TableError>;
85    fn copy(
86        &self,
87        source: &ReadTransaction,
88        destination: &mut WriteTransaction,
89    ) -> std::result::Result<(), DbCopyError>;
90
91    fn display_name(&self) -> String {
92        format!("{} {}", self.kind(), self.name())
93    }
94}
95
96/// Builder for a database copy plan.
97#[derive(Default)]
98pub struct CopyPlan {
99    steps: Vec<Box<dyn CopyStep>>,
100}
101
102impl CopyPlan {
103    /// Create a new empty copy plan.
104    pub fn new() -> Self {
105        Self { steps: Vec::new() }
106    }
107
108    /// Add a normal table to the copy plan.
109    pub fn table<K: redb::Key + 'static, V: redb::Value + 'static>(
110        mut self,
111        table: TableDefinition<'_, K, V>,
112    ) -> Self {
113        self.steps.push(Box::new(TablePlan::new(table)));
114        self
115    }
116
117    /// Add a multimap table to the copy plan.
118    pub fn multimap<K: redb::Key + 'static, V: redb::Key + 'static>(
119        mut self,
120        table: MultimapTableDefinition<'_, K, V>,
121    ) -> Self {
122        self.steps.push(Box::new(MultimapPlan::new(table)));
123        self
124    }
125}
126
127/// Copy all tables described by `plan` from `source` to `destination`.
128pub fn copy_database(source: &Database, destination: &Database, plan: &CopyPlan) -> Result<()> {
129    let source_read = source
130        .begin_read()
131        .map_err(|err| DbCopyError::TransactionFailed(format!("source read: {}", err)))?;
132    let destination_read = destination
133        .begin_read()
134        .map_err(|err| DbCopyError::TransactionFailed(format!("destination read: {}", err)))?;
135
136    let mut conflicts = Vec::new();
137    for step in &plan.steps {
138        match step.preflight(&destination_read) {
139            Ok(true) => conflicts.push(step.display_name()),
140            Ok(false) => {}
141            Err(err) => {
142                return Err(DbCopyError::DestinationCheckFailed(format!(
143                    "{}: {}",
144                    step.display_name(),
145                    err
146                ))
147                .into())
148            }
149        }
150    }
151
152    if !conflicts.is_empty() {
153        return Err(DbCopyError::DestinationTablesExist(conflicts).into());
154    }
155
156    drop(destination_read);
157
158    let mut destination_write = destination
159        .begin_write()
160        .map_err(|err| DbCopyError::TransactionFailed(format!("destination write: {}", err)))?;
161
162    for step in &plan.steps {
163        step.copy(&source_read, &mut destination_write)?;
164    }
165
166    destination_write
167        .commit()
168        .map_err(|err| DbCopyError::CommitFailed(err.to_string()))?;
169
170    Ok(())
171}
172
173struct TablePlan<K: redb::Key + 'static, V: redb::Value + 'static> {
174    name: String,
175    _key: PhantomData<K>,
176    _value: PhantomData<V>,
177}
178
179impl<K: redb::Key + 'static, V: redb::Value + 'static> TablePlan<K, V> {
180    fn new(table: TableDefinition<'_, K, V>) -> Self {
181        Self {
182            name: table.name().to_string(),
183            _key: PhantomData,
184            _value: PhantomData,
185        }
186    }
187
188    fn definition(&self) -> TableDefinition<'_, K, V> {
189        TableDefinition::new(self.name.as_str())
190    }
191}
192
193impl<K: redb::Key + 'static, V: redb::Value + 'static> CopyStep for TablePlan<K, V> {
194    fn name(&self) -> &str {
195        &self.name
196    }
197
198    fn kind(&self) -> CopyKind {
199        CopyKind::Table
200    }
201
202    fn preflight(&self, destination: &ReadTransaction) -> std::result::Result<bool, TableError> {
203        match destination.open_table(self.definition()) {
204            Ok(_) => Ok(true),
205            Err(TableError::TableDoesNotExist(_)) => Ok(false),
206            Err(err) => Err(err),
207        }
208    }
209
210    fn copy(
211        &self,
212        source: &ReadTransaction,
213        destination: &mut WriteTransaction,
214    ) -> std::result::Result<(), DbCopyError> {
215        let source_table = source.open_table(self.definition()).map_err(|err| {
216            DbCopyError::SourceTableOpenFailed(format!("{}: {}", self.display_name(), err))
217        })?;
218        let mut destination_table = destination.open_table(self.definition()).map_err(|err| {
219            DbCopyError::DestinationTableOpenFailed(format!("{}: {}", self.display_name(), err))
220        })?;
221        let iter = source_table.iter().map_err(|err| {
222            DbCopyError::TableCopyFailed(format!("{}: {}", self.display_name(), err))
223        })?;
224
225        for entry in iter {
226            let (key, value) = entry.map_err(|err| {
227                DbCopyError::TableCopyFailed(format!("{}: {}", self.display_name(), err))
228            })?;
229            destination_table
230                .insert(key.value(), value.value())
231                .map_err(|err| {
232                    DbCopyError::TableCopyFailed(format!("{}: {}", self.display_name(), err))
233                })?;
234        }
235
236        Ok(())
237    }
238}
239
240struct MultimapPlan<K: redb::Key + 'static, V: redb::Key + 'static> {
241    name: String,
242    _key: PhantomData<K>,
243    _value: PhantomData<V>,
244}
245
246impl<K: redb::Key + 'static, V: redb::Key + 'static> MultimapPlan<K, V> {
247    fn new(table: MultimapTableDefinition<'_, K, V>) -> Self {
248        Self {
249            name: table.name().to_string(),
250            _key: PhantomData,
251            _value: PhantomData,
252        }
253    }
254
255    fn definition(&self) -> MultimapTableDefinition<'_, K, V> {
256        MultimapTableDefinition::new(self.name.as_str())
257    }
258}
259
260impl<K: redb::Key + 'static, V: redb::Key + 'static> CopyStep for MultimapPlan<K, V> {
261    fn name(&self) -> &str {
262        &self.name
263    }
264
265    fn kind(&self) -> CopyKind {
266        CopyKind::Multimap
267    }
268
269    fn preflight(&self, destination: &ReadTransaction) -> std::result::Result<bool, TableError> {
270        match destination.open_multimap_table(self.definition()) {
271            Ok(_) => Ok(true),
272            Err(TableError::TableDoesNotExist(_)) => Ok(false),
273            Err(err) => Err(err),
274        }
275    }
276
277    fn copy(
278        &self,
279        source: &ReadTransaction,
280        destination: &mut WriteTransaction,
281    ) -> std::result::Result<(), DbCopyError> {
282        let source_table = source
283            .open_multimap_table(self.definition())
284            .map_err(|err| {
285                DbCopyError::SourceTableOpenFailed(format!("{}: {}", self.display_name(), err))
286            })?;
287        let mut destination_table =
288            destination
289                .open_multimap_table(self.definition())
290                .map_err(|err| {
291                    DbCopyError::DestinationTableOpenFailed(format!(
292                        "{}: {}",
293                        self.display_name(),
294                        err
295                    ))
296                })?;
297        let iter = source_table.iter().map_err(|err| {
298            DbCopyError::TableCopyFailed(format!("{}: {}", self.display_name(), err))
299        })?;
300
301        for entry in iter {
302            let (key, values) = entry.map_err(|err| {
303                DbCopyError::TableCopyFailed(format!("{}: {}", self.display_name(), err))
304            })?;
305            for value in values {
306                let value = value.map_err(|err| {
307                    DbCopyError::TableCopyFailed(format!("{}: {}", self.display_name(), err))
308                })?;
309                destination_table
310                    .insert(key.value(), value.value())
311                    .map_err(|err| {
312                        DbCopyError::TableCopyFailed(format!("{}: {}", self.display_name(), err))
313                    })?;
314            }
315        }
316
317        Ok(())
318    }
319}