mini_sqlite_dump/
lib.rs

1#![deny(missing_docs)]
2#![doc=include_str!("../README.md")]
3
4use std::collections::{HashMap, VecDeque};
5use std::fmt::{self, Display, Write as _};
6use std::io;
7use std::iter;
8use std::mem;
9use std::ops;
10use std::str;
11
12use anyhow::{anyhow, Context as _};
13use derive_deftly::{Deftly, define_derive_deftly};
14use derive_more::From;
15use easy_ext::ext;
16use hex_fmt::HexFmt;
17use itertools::{chain, izip, Itertools};
18use rusqlite::{Transaction, types::ValueRef};
19use thiserror::Error;
20
21mod text;
22use text::write_text;
23mod real;
24use real::write_real;
25
26#[cfg(test)]
27mod test;
28
29/// **Writer of sqlite3 data - toplevel entrypoint**
30///
31/// `W` should be buffered.
32/// After an error, it is not safe to continue to write to `W`
33/// (with these facilities, or any other):
34/// partial data may have been written,
35/// so the output should then be treated as corrupted and useless.
36///
37/// To use this:
38///
39///  * Call [`Archiver::start()`] and record the `table_names`.
40///  * For each table, call [`start_table`](Archiver::start_table),
41///    get the rows you want out with a normal database query,
42///    and call [`TableArchiver::write_row`] on each one.
43///  * At the end, call [`finish`](Archiver::finish).
44pub struct Archiver<W> {
45    w: W,
46    tables: VecDeque<TableInfo>
47}
48
49struct TableInfo {
50    name: String,
51    cols: Vec<String>,
52}
53
54/// Handle for writing individual rows with an [`Archiver`].
55pub struct TableArchiver<'a, W> {
56    a: &'a mut Archiver<W>,
57    t: TableInfo,
58}
59
60/// Error found while writing a database dump
61#[derive(Error, Debug)]
62pub enum Error {
63    /// IO error on the writer `W`
64    #[error("{0}")]
65    Io(#[from] io::Error),
66
67    /// Database contains something unsupported by `mini-sqlite-dump`
68    #[error("lack of support for these database contents: {0:#}")]
69    Unsupported(anyhow::Error),
70
71    /// Database operation unexpectedly failed
72    ///
73    /// This could mean that the caller is driving `mini-sqlite-dump` wrongly.
74    #[error("database operation failed: {0:#}")]
75    Db(anyhow::Error),
76
77    /// Internal error in `mini-sqlite-dump` (bug)
78    #[error("internal error: {0:#}")]
79    Internal(anyhow::Error),
80}
81
82type E = Error;
83
84impl<W: io::Write> Archiver<W> {
85    /// Start writing a dump.
86    ///
87    /// Will enumerate the tables found in the database,
88    /// and pass their names to `table_names`.
89    ///
90    /// This can be used to enumerate over all tables;
91    /// or they can be ignored if only certain tables need to be dumped.
92    ///
93    /// (The schema for every existing table will be dumped, unconditionally;
94    /// there is not currently a way to control this.)
95    pub fn start<S: Into<String>>(
96        dbt: &Transaction,
97        mut w: W,
98        table_names: impl IntoIterator<Item = S>,
99    ) -> Result<Self, Error> {
100        let mut tables = VecDeque::new();
101
102        write!(w, include_str!("header.sql"))?;
103
104        let encoding: String  = dbt.query_row(
105            r#" PRAGMA encoding "#, [],
106            |row| row.get(0)
107        )
108            .context("execute encoding access pragma").map_err(E::Db)?;
109
110        const EXPECTED_ENCODING: &str = "UTF-8";
111        if &encoding != EXPECTED_ENCODING {
112            return Err(E::Unsupported(anyhow!(
113 "database encoding is {encoding:?}, only {EXPECTED_ENCODING:?} is supported"
114            )));
115        }
116
117        let mut schema_stmt = dbt.prepare(
118            r#" SELECT sql FROM 'SQLITE_SCHEMA'
119                 WHERE type = 'table' AND name = ? "#
120        ).context("prepare schema access query").map_err(E::Db)?;
121
122        for name in table_names {
123            let name: String = name.into();
124            
125            let sql: String = schema_stmt.query_row(
126                [&name],
127                |row| Ok(row.get(0)),
128            )
129                .context("execute schema access query").map_err(E::Db)?
130                .context("obtain schema text from row").map_err(E::Db)?;
131
132            write!(w, "{};\n", sql)?;
133
134            let pragma = format!(r#" PRAGMA table_xinfo('{name}') "#);
135
136            let mut cols_stmt = dbt.prepare({
137                assert!(! name.contains(|c| c=='\'' || c=='\0'));
138                &pragma
139            }).context("prepare PRAGMA table_inf query").map_err(E::Db)?;
140
141            let cols = cols_stmt.query([])
142                .context("execute PRAGMA table_xinfo").map_err(E::Db)?
143                .mapped(|row| row.get("name"))
144                .collect::<Result<Vec<String>, _>>()
145                .context("read/convert PRAGMA table_xinfo rows")
146                    .map_err(E::Db)?;
147
148            tables.push_back(TableInfo {
149                name,
150                cols,
151            });
152        }
153
154        let self_ = Archiver {
155            w,
156            tables,
157        };
158        Ok(self_)
159    }
160
161    /// Start writing a dump of a particular table.
162    pub fn start_table(&mut self, name: &str) -> Result<TableArchiver<W>, E> {
163        let t = self.tables.pop_front()
164            .ok_or_else(|| internal_error(
165                anyhow!("start_table called too many times")
166            ))?;
167
168        if t.name != name {
169            return Err(internal_error(anyhow!(
170                "expected start_table({}), got start_table({name})",
171                t.name,
172            )));
173        }
174        
175        Ok(TableArchiver {
176            a: self,
177            t,
178        })
179    }
180
181    /// Finish writing the dump.
182    ///
183    /// The writer `W` will be flushed and then dropped.
184    pub fn finish(self) -> Result<(), E> {
185        self.finish_with_writer()?;
186        Ok(())
187    }
188
189    /// Finish writing the dump, returning the writer.
190    ///
191    /// The writer `W` will be flushed.
192    pub fn finish_with_writer(mut self) -> Result<W, E> {
193        if ! self.tables.is_empty() {
194            let e = anyhow!(
195                "tables unprocessed at finish! {:?}",
196                self.tables.iter().map(|ti| &ti.name).collect_vec()
197            );
198            return Err(internal_error(e));
199        }
200
201        write!(self.w, "COMMIT;\n")?;
202        self.w.flush()?;
203        Ok(self.w)
204    }
205
206    /// Access the inner writer
207    ///
208    /// Take care!  Using this to write will probably make data corruption.
209    pub fn writer_mut(&mut self) -> &mut W {
210        &mut self.w
211    }
212}
213
214/// Row data, that can be archived
215pub trait RowLike {
216    /// Get an individual data value, by its field name
217    fn get_by_name(&self, n: &str) -> rusqlite::Result<ValueRef>;
218
219    /// Check that the supplied data has at most `l` fields
220    ///
221    /// If `self` has more than `l` fields, returns an error.
222    /// If it has no more than `l`, returns `Ok(())`.
223    ///
224    /// Used by [`TableArchiver::write_row`]
225    /// to check that it has really archived all the data in the row.
226    fn check_max_len(&self, l: usize) -> anyhow::Result<()>;
227}
228
229impl RowLike for rusqlite::Row<'_> {
230    fn get_by_name(&self, n: &str) -> rusqlite::Result<ValueRef> {
231        self.get_ref(n)
232    }
233    fn check_max_len(&self, l: usize) -> anyhow::Result<()> {
234        match self.get_ref(l) {
235            Err(rusqlite::Error::InvalidColumnIndex { .. }) => Ok(()),
236            Err(other) => Err(
237                anyhow::Error::from(other) // we have row already, so
238                                           // not deadlock/timeout
239                .context(
240                    "get out of range column failed in an unexpected way!"
241                )),
242            Ok(_) => Err(anyhow!(
243                "get out of range column succeeded!"
244            )),
245        }
246    }
247}
248
249impl RowLike for HashMap<&str, ValueRef<'_>> {
250    fn get_by_name(&self, n: &str) -> rusqlite::Result<ValueRef> {
251        self.get(n)
252            .copied()
253            .ok_or_else(|| rusqlite::Error::InvalidColumnName(n.into()))
254    }
255    fn check_max_len(&self, l: usize) -> anyhow::Result<()> {
256        if self.len() <= l {
257            Ok(())
258        } else {
259            Err(anyhow!("row has {} rows, expected at most {l}", self.len()))
260        }
261    }
262}
263
264impl<W: io::Write> TableArchiver<'_, W> {
265    /// Write a single row.
266    ///
267    /// The row can be a `Row` (for example, returned from a query),
268    /// a `HashMap`, or something else implementing `RowLike`.
269    ///
270    /// The fields in `row` must match those in the actual table.
271    pub fn write_row(
272        &mut self,
273        row: &impl RowLike,
274    ) -> Result<(), Error> {
275        let mut w = &mut self.a.w;
276        let t = &self.t;
277        write!(w, "INSERT INTO {} VALUES (", t.name)?;
278
279        row.check_max_len(t.cols.len()).map_err(internal_error)?;
280            
281        for (delim, col) in izip!(
282            chain!([""], iter::repeat(",")),
283            &t.cols,
284        ) {
285            write!(w, "{delim}")?;
286            let v = row.get_by_name(col)
287                .with_context(|| format!("table {:?}", t.name))
288                .context("fetch data row")
289                .map_err(E::Db)?;
290
291            write_value(&mut w, v)?;
292        }
293
294        write!(w, ");\n")?;
295
296        Ok(())
297    }
298
299    /// Access the inner writer
300    ///
301    /// Take care!  Using this to write will probably make data corruption.
302    pub fn writer_mut(&mut self) -> &mut W {
303        &mut self.a.w
304    }
305}
306
307/// Dump a single `rusqlite::ValueRef` in textual format.
308///
309/// The output syntax is a sqlite3 value expression, in UTF-8.
310///
311/// This utility method is exposed for completeness;
312/// callers using [`Archiver`] do not need it.
313pub fn write_value(mut w: impl io::Write, v: ValueRef<'_>) -> Result<(), E> {
314    use ValueRef as V;
315    match v {
316        V::Null => write!(w, "NULL")?,
317        V::Integer(i) => write!(w, "{i}")?,
318        V::Real(v) => write_real(w, v)?,
319        V::Blob(b) => write!(w, "x'{}'", HexFmt(b))?,
320        V::Text(t) => write_text(w, t)?,
321    };
322    Ok(())
323}
324
325fn internal_error(ae: anyhow::Error) -> E {
326    Error::Internal(ae)
327}