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