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 user_version: i64  = dbt.query_row(
105            r#" PRAGMA user_version "#, [],
106            |row| row.get(0)
107        )
108            .context("execute user_version access pragma").map_err(E::Db)?;
109
110        write!(w, "-- PRAGMA user_version = {user_version};\n")?;
111
112        let encoding: String  = dbt.query_row(
113            r#" PRAGMA encoding "#, [],
114            |row| row.get(0)
115        )
116            .context("execute encoding access pragma").map_err(E::Db)?;
117
118        const EXPECTED_ENCODING: &str = "UTF-8";
119        if &encoding != EXPECTED_ENCODING {
120            return Err(E::Unsupported(anyhow!(
121 "database encoding is {encoding:?}, only {EXPECTED_ENCODING:?} is supported"
122            )));
123        }
124
125        let mut schema_stmt = dbt.prepare(
126            r#" SELECT sql FROM 'SQLITE_SCHEMA'
127                 WHERE type = 'table' AND name = ? "#
128        ).context("prepare schema access query").map_err(E::Db)?;
129
130        for name in table_names {
131            let name: String = name.into();
132            
133            let sql: String = schema_stmt.query_row(
134                [&name],
135                |row| Ok(row.get(0)),
136            )
137                .context("execute schema access query").map_err(E::Db)?
138                .context("obtain schema text from row").map_err(E::Db)?;
139
140            write!(w, "{};\n", sql)?;
141
142            let pragma = format!(r#" PRAGMA table_xinfo('{name}') "#);
143
144            let mut cols_stmt = dbt.prepare({
145                assert!(! name.contains(|c| c=='\'' || c=='\0'));
146                &pragma
147            }).context("prepare PRAGMA table_inf query").map_err(E::Db)?;
148
149            let cols = cols_stmt.query([])
150                .context("execute PRAGMA table_xinfo").map_err(E::Db)?
151                .mapped(|row| row.get("name"))
152                .collect::<Result<Vec<String>, _>>()
153                .context("read/convert PRAGMA table_xinfo rows")
154                    .map_err(E::Db)?;
155
156            tables.push_back(TableInfo {
157                name,
158                cols,
159            });
160        }
161
162        let self_ = Archiver {
163            w,
164            tables,
165        };
166        Ok(self_)
167    }
168
169    /// Start writing a dump of a particular table.
170    pub fn start_table(&mut self, name: &str)
171                       -> Result<TableArchiver<'_, W>, E>
172    {
173        let t = self.tables.pop_front()
174            .ok_or_else(|| internal_error(
175                anyhow!("start_table called too many times")
176            ))?;
177
178        if t.name != name {
179            return Err(internal_error(anyhow!(
180                "expected start_table({}), got start_table({name})",
181                t.name,
182            )));
183        }
184        
185        Ok(TableArchiver {
186            a: self,
187            t,
188        })
189    }
190
191    /// Finish writing the dump.
192    ///
193    /// The writer `W` will be flushed and then dropped.
194    pub fn finish(self) -> Result<(), E> {
195        self.finish_with_writer()?;
196        Ok(())
197    }
198
199    /// Finish writing the dump, returning the writer.
200    ///
201    /// The writer `W` will be flushed.
202    pub fn finish_with_writer(mut self) -> Result<W, E> {
203        if ! self.tables.is_empty() {
204            let e = anyhow!(
205                "tables unprocessed at finish! {:?}",
206                self.tables.iter().map(|ti| &ti.name).collect_vec()
207            );
208            return Err(internal_error(e));
209        }
210
211        write!(self.w, "COMMIT;\n")?;
212        self.w.flush()?;
213        Ok(self.w)
214    }
215
216    /// Access the inner writer
217    ///
218    /// Take care!  Using this to write will probably make data corruption.
219    pub fn writer_mut(&mut self) -> &mut W {
220        &mut self.w
221    }
222}
223
224/// Row data, that can be archived
225pub trait RowLike {
226    /// Get an individual data value, by its field name
227    fn get_by_name(&self, n: &str) -> rusqlite::Result<ValueRef<'_>>;
228
229    /// Check that the supplied data has at most `l` fields
230    ///
231    /// If `self` has more than `l` fields, returns an error.
232    /// If it has no more than `l`, returns `Ok(())`.
233    ///
234    /// Used by [`TableArchiver::write_row`]
235    /// to check that it has really archived all the data in the row.
236    fn check_max_len(&self, l: usize) -> anyhow::Result<()>;
237}
238
239impl RowLike for rusqlite::Row<'_> {
240    fn get_by_name(&self, n: &str) -> rusqlite::Result<ValueRef<'_>> {
241        self.get_ref(n)
242    }
243    fn check_max_len(&self, l: usize) -> anyhow::Result<()> {
244        match self.get_ref(l) {
245            Err(rusqlite::Error::InvalidColumnIndex { .. }) => Ok(()),
246            Err(other) => Err(
247                anyhow::Error::from(other) // we have row already, so
248                                           // not deadlock/timeout
249                .context(
250                    "get out of range column failed in an unexpected way!"
251                )),
252            Ok(_) => Err(anyhow!(
253                "get out of range column succeeded!"
254            )),
255        }
256    }
257}
258
259impl RowLike for HashMap<&str, ValueRef<'_>> {
260    fn get_by_name(&self, n: &str) -> rusqlite::Result<ValueRef<'_>> {
261        self.get(n)
262            .copied()
263            .ok_or_else(|| rusqlite::Error::InvalidColumnName(n.into()))
264    }
265    fn check_max_len(&self, l: usize) -> anyhow::Result<()> {
266        if self.len() <= l {
267            Ok(())
268        } else {
269            Err(anyhow!("row has {} rows, expected at most {l}", self.len()))
270        }
271    }
272}
273
274impl<W: io::Write> TableArchiver<'_, W> {
275    /// Write a single row.
276    ///
277    /// The row can be a `Row` (for example, returned from a query),
278    /// a `HashMap`, or something else implementing `RowLike`.
279    ///
280    /// The fields in `row` must match those in the actual table.
281    pub fn write_row(
282        &mut self,
283        row: &impl RowLike,
284    ) -> Result<(), Error> {
285        let mut w = &mut self.a.w;
286        let t = &self.t;
287        write!(w, "INSERT INTO {} VALUES (", t.name)?;
288
289        row.check_max_len(t.cols.len()).map_err(internal_error)?;
290            
291        for (delim, col) in izip!(
292            chain!([""], iter::repeat(",")),
293            &t.cols,
294        ) {
295            write!(w, "{delim}")?;
296            let v = row.get_by_name(col)
297                .with_context(|| format!("table {:?}", t.name))
298                .context("fetch data row")
299                .map_err(E::Db)?;
300
301            write_value(&mut w, v)?;
302        }
303
304        write!(w, ");\n")?;
305
306        Ok(())
307    }
308
309    /// Access the inner writer
310    ///
311    /// Take care!  Using this to write will probably make data corruption.
312    pub fn writer_mut(&mut self) -> &mut W {
313        &mut self.a.w
314    }
315}
316
317/// Dump a single `rusqlite::ValueRef` in textual format.
318///
319/// The output syntax is a sqlite3 value expression, in UTF-8.
320///
321/// This utility method is exposed for completeness;
322/// callers using [`Archiver`] do not need it.
323pub fn write_value(mut w: impl io::Write, v: ValueRef<'_>) -> Result<(), E> {
324    use ValueRef as V;
325    match v {
326        V::Null => write!(w, "NULL")?,
327        V::Integer(i) => write!(w, "{i}")?,
328        V::Real(v) => write_real(w, v)?,
329        V::Blob(b) => write!(w, "x'{}'", HexFmt(b))?,
330        V::Text(t) => write_text(w, t)?,
331    };
332    Ok(())
333}
334
335fn internal_error(ae: anyhow::Error) -> E {
336    Error::Internal(ae)
337}