Skip to main content

inillucent_cli/
dump.rs

1//! `.dump`: the SQL that would rebuild the database.
2//!
3//! Invariant: what comes out reproduces what went in, and it says so at the
4//! top. A dump is wrapped in a transaction and begins with
5//! `PRAGMA foreign_keys=OFF`, because the rows come out in `sqlite_master`
6//! order rather than in dependency order and a foreign key would refuse a child
7//! whose parent is three statements later. That is SQLite's dump format, and a
8//! dump that could not be fed back in is not a dump.
9
10use inillucent_value::Value;
11
12use crate::render::literal;
13use crate::shell::Shell;
14
15/// Writes the whole database, or one table, as SQL.
16///
17/// The order and the special cases are the reference's, because a dump is
18/// compared to the reference's byte for byte: tables first with their rows,
19/// `sqlite_sequence` last among them, then the indexes, triggers and views in
20/// the order they were created.
21pub fn dump(shell: &mut Shell, pattern: Option<&str>) {
22    let tables = table_objects(shell, pattern);
23    // The warning goes above everything, including the `PRAGMA`, because a
24    // virtual table is restored by writing `sqlite_schema` directly and a
25    // defensive connection refuses that. The reference prints it first and so
26    // does this.
27    if tables
28        .iter()
29        .any(|(_, sql)| sql.starts_with("CREATE VIRTUAL TABLE"))
30    {
31        shell.say("/* WARNING: Script requires that SQLITE_DBCONFIG_DEFENSIVE be disabled */");
32    }
33    shell.say("PRAGMA foreign_keys=OFF;");
34    shell.say("BEGIN TRANSACTION;");
35    let mut writable = false;
36    for (name, sql) in &tables {
37        say_definition(shell, name, sql, &mut writable);
38        // A virtual table's rows live in its shadow tables, which are dumped as
39        // the ordinary tables they are. Dumping them here as well would insert
40        // every row a second time, through a module that is about to rebuild
41        // them from the shadows.
42        if sql.starts_with("CREATE VIRTUAL TABLE") {
43            continue;
44        }
45        write_rows(shell, name, sql);
46    }
47    for sql in other_objects(shell, pattern) {
48        shell.say(&format!("{sql};"));
49    }
50    if writable {
51        shell.say("PRAGMA writable_schema=OFF;");
52    }
53    shell.say("COMMIT;");
54}
55
56/// Returns the tables a dump covers, `sqlite_sequence` last.
57///
58/// **The reserved names are in, not out.** `sqlite_sequence` carries the
59/// AUTOINCREMENT high-water mark and `sqlite_stat1` carries the measurements: a
60/// dump that dropped them rebuilds a database that reuses deleted keys and
61/// plans every join by guesswork. The reference carries both, each in its own
62/// form, and so does this.
63///
64/// @param shell - the shell to ask
65/// @param pattern - the `LIKE` pattern `.dump ?OBJECTS?` named, if any
66fn table_objects(shell: &Shell, pattern: Option<&str>) -> Vec<(String, String)> {
67    let mut sql = String::from(
68        "SELECT name, sql FROM sqlite_master WHERE type = 'table' AND sql IS NOT NULL",
69    );
70    if let Some(pattern) = pattern {
71        let quoted = format!("'{}'", pattern.replace('\'', "''"));
72        sql.push_str(&format!(
73            " AND (name LIKE {quoted} OR tbl_name LIKE {quoted})"
74        ));
75    }
76    sql.push_str(" ORDER BY tbl_name = 'sqlite_sequence', rowid");
77    let Ok((_, rows)) = shell.collect(&sql) else {
78        return Vec::new();
79    };
80    rows.iter()
81        .map(|row| (plain(row.first()), plain(row.get(1))))
82        .collect()
83}
84
85/// Returns the indexes, triggers and views a dump covers, in creation order.
86///
87/// By type descending and then by creation, which is views, then triggers,
88/// then indexes - the order the reference emits and a replayable one, since a
89/// trigger may name a view.
90///
91/// @param shell - the shell to ask
92/// @param pattern - the `LIKE` pattern `.dump ?OBJECTS?` named, if any
93fn other_objects(shell: &Shell, pattern: Option<&str>) -> Vec<String> {
94    let mut sql = String::from(
95        "SELECT sql FROM sqlite_master \
96         WHERE sql IS NOT NULL AND type IN ('index', 'trigger', 'view')",
97    );
98    if let Some(pattern) = pattern {
99        let quoted = format!("'{}'", pattern.replace('\'', "''"));
100        sql.push_str(&format!(
101            " AND (name LIKE {quoted} OR tbl_name LIKE {quoted})"
102        ));
103    }
104    sql.push_str(" ORDER BY type DESC, rowid");
105    let Ok((_, rows)) = shell.collect(&sql) else {
106        return Vec::new();
107    };
108    rows.iter().map(|row| plain(row.first())).collect()
109}
110
111/// Returns a value as plain text.
112fn plain(value: Option<&Value<'static>>) -> String {
113    match value {
114        Some(Value::Text(text)) => String::from_utf8_lossy(text.raw()).into_owned(),
115        Some(Value::Null) | None => String::new(),
116        Some(other) => literal(other),
117    }
118}
119
120/// Writes one table's definition, in the form the reference writes it.
121///
122/// Four forms, and which one a table takes is what makes a dump replayable:
123///
124/// - a statistics table is not recreated at all, because `ANALYZE` recreates
125///   it; the line that stands in for it is `ANALYZE sqlite_schema`, which is
126///   what makes the target read the rows that follow;
127/// - any other reserved name is recreated with `IF NOT EXISTS` over a writable
128///   schema, because the target already has one and the rows have to land in
129///   the copy it has;
130/// - a virtual table is a row written into `sqlite_schema` rather than a
131///   `CREATE`, because running its `CREATE VIRTUAL TABLE` would build empty
132///   shadow tables over the ones the dump is about to restore;
133/// - and an ordinary table is its own text, with `IF NOT EXISTS` inserted when
134///   its name was quoted - which is the reference's rule, and is what covers
135///   the shadow tables a module names with quotes.
136///
137/// @param shell - where the lines go
138/// @param name - the table's name
139/// @param sql - the `CREATE` text the schema holds
140/// @param writable - whether `PRAGMA writable_schema=ON` has been written yet
141fn say_definition(shell: &mut Shell, name: &str, sql: &str, writable: &mut bool) {
142    /// What `CREATE TABLE ` occupies, which is what the rewrite skips.
143    const CREATE_TABLE: usize = 13;
144
145    if is_statistics_table(name) {
146        shell.say("ANALYZE sqlite_schema;");
147        return;
148    }
149    if name.len() >= 7 && name[..7].eq_ignore_ascii_case("sqlite_") {
150        make_writable(shell, writable);
151        if let Some(rest) = sql.get(CREATE_TABLE..) {
152            shell.say(&format!("CREATE TABLE IF NOT EXISTS {rest};"));
153        }
154        if name.eq_ignore_ascii_case("sqlite_sequence") {
155            shell.say("DELETE FROM sqlite_sequence;");
156        }
157        return;
158    }
159    if sql.starts_with("CREATE VIRTUAL TABLE") {
160        make_writable(shell, writable);
161        let escaped_name = name.replace('\'', "''");
162        let escaped_sql = sql.replace('\'', "''");
163        shell.say(&format!(
164            "INSERT INTO sqlite_schema(type,name,tbl_name,rootpage,sql)VALUES('table','{escaped_name}','{escaped_name}',0,'{escaped_sql}');"
165        ));
166        return;
167    }
168    let quoted_name = sql.len() > CREATE_TABLE
169        && sql[..CREATE_TABLE].eq_ignore_ascii_case("CREATE TABLE ")
170        && sql[CREATE_TABLE..].starts_with(['"', '\'']);
171    if quoted_name {
172        shell.say(&format!(
173            "CREATE TABLE IF NOT EXISTS {};",
174            &sql[CREATE_TABLE..]
175        ));
176        return;
177    }
178    shell.say(&format!("{sql};"));
179}
180
181/// Writes `PRAGMA writable_schema=ON` the first time it is needed.
182///
183/// @param shell - where the line goes
184/// @param writable - whether it has already been written
185fn make_writable(shell: &mut Shell, writable: &mut bool) {
186    if *writable {
187        return;
188    }
189    shell.say("PRAGMA writable_schema=ON;");
190    *writable = true;
191}
192
193/// Reports whether a name is one of the statistics tables.
194///
195/// `sqlite_stat1` and `sqlite_stat4`, matched the way the reference matches
196/// them: the prefix and exactly one more character.
197///
198/// @param name - the table's name
199fn is_statistics_table(name: &str) -> bool {
200    name.len() == 12 && name[..11].eq_ignore_ascii_case("sqlite_stat")
201}
202
203/// Returns the columns a dump writes values for, in declaration order.
204///
205/// **`PRAGMA table_info`, not `SELECT *`.** A generated column is in `*` and is
206/// not in `table_info`, which is the distinction that matters here: its value is
207/// derived on read, so writing it back is writing a column the target refuses to
208/// be given. `.dump` of a table with two generated columns emitted four values
209/// per row and the reference answered
210/// `table t has 3 columns but 6 values were supplied` - a dump that only this
211/// engine could replay.
212///
213/// Empty when the pragma cannot be read, which the caller turns back into
214/// `SELECT *` rather than dumping nothing.
215///
216/// **A column whose name is empty is a column** (task-2066 §4.1.3). This used
217/// to drop it, so `CREATE TABLE d4 ("" TEXT, b INT)` holding one row dumped as
218/// `INSERT INTO d4 VALUES(5)` - one value for two columns. Replaying that puts
219/// `5` in the first column and leaves the second null, which is worse than
220/// losing the row: the restored database holds different data and nothing says
221/// so. `""` is a legal column name and the pinned SQLite 3.53.4 dumps both of
222/// its values.
223///
224/// @param shell - the shell to ask
225/// @param table - the table's name
226fn stored_columns(shell: &mut Shell, table: &str) -> Vec<String> {
227    let literal = format!("'{}'", table.replace('\'', "''"));
228    let Ok((_, rows)) = shell.collect(&format!("SELECT name FROM pragma_table_info({literal})"))
229    else {
230        return Vec::new();
231    };
232    rows.iter().map(|row| plain(row.first())).collect()
233}
234
235/// Writes every row of one table as an `INSERT`.
236///
237/// **The projection quotes every name; the emitted `INSERT` quotes none**
238/// (task-2066 §4.1.3). The two are different jobs and used to share one rule.
239/// `quote_identifier` exists so the *emitted* text reads the way the reference
240/// writes it - a bare word is left bare - and applying that to the `SELECT`
241/// this reads the rows with produced `SELECT select,b FROM d1` for
242/// `CREATE TABLE d1 ("select" TEXT, b INT)`. That is a syntax error,
243/// `shell.collect` answered `Err`, and this returned having written no `INSERT`
244/// at all: the table's `CREATE` in the dump, its rows gone, exit 0. A column
245/// named `"order"` and a table named `"select"` did the same.
246///
247/// Losing rows at exit 0 is the worst shape a backup tool can have, so the
248/// failure is no longer silent either: a table whose rows cannot be read
249/// complains, which sets the shell's failure flag and makes the verb exit
250/// non-zero.
251///
252/// @param shell - where the lines go
253/// @param table - the table to write
254/// @param sql - the `CREATE` text the schema holds, which decides how the
255///   emitted name is spelled
256fn write_rows(shell: &mut Shell, table: &str, sql: &str) {
257    let quoted = emitted_name(table, sql);
258    let columns = stored_columns(shell, table);
259    // A plain `VALUES` list with no column names, which is what the reference
260    // writes: an unnamed insert maps positionally onto the columns that are not
261    // generated, so the two agree without naming anything.
262    let projection = if columns.is_empty() {
263        "*".to_string()
264    } else {
265        columns
266            .iter()
267            .map(|name| always_quoted(name))
268            .collect::<Vec<String>>()
269            .join(",")
270    };
271    // The table name is quoted here too, for the same reason and independently
272    // of how it is emitted: `SELECT * FROM select` does not parse either.
273    let reading = format!("SELECT {projection} FROM {}", always_quoted(table));
274    let Ok((_, rows)) = shell.collect(&reading) else {
275        shell.complain(&format!(
276            "-- the rows of {table} could not be read, so none are in this dump"
277        ));
278        return;
279    };
280    for row in rows {
281        let values: Vec<String> = row.iter().map(literal).collect();
282        shell.say(&format!(
283            "INSERT INTO {quoted} VALUES({});",
284            values.join(",")
285        ));
286    }
287}
288
289/// Returns the table name as the emitted `INSERT` spells it.
290///
291/// **The reference's rule is "however the `CREATE` spelled it", not "quote it
292/// when it needs quoting".** Asked the same schema, the pinned SQLite 3.53.4
293/// writes `INSERT INTO d1 VALUES('x',1)` for a table whose column is named
294/// `"select"`, and `INSERT INTO "select" VALUES('v')` for a table *named*
295/// `select` - because that `CREATE` carried the quotes. `quote_identifier`
296/// alone answers the first correctly and the second wrongly, since `select` is
297/// a bare word by its rule. `interchange.rs` compares this output with the
298/// reference's byte for byte, so the difference is a failure rather than a
299/// preference.
300///
301/// This is the same signal `say_definition` reads to decide whether to write
302/// `IF NOT EXISTS`, so the `CREATE` and the `INSERT` agree by construction.
303///
304/// @param table - the table's name
305/// @param sql - the `CREATE` text the schema holds
306fn emitted_name(table: &str, sql: &str) -> String {
307    /// What `CREATE TABLE ` occupies.
308    const CREATE_TABLE: usize = 13;
309
310    let quoted_in_the_schema = sql.len() > CREATE_TABLE
311        && sql
312            .get(..CREATE_TABLE)
313            .is_some_and(|head| head.eq_ignore_ascii_case("CREATE TABLE "))
314        && sql
315            .get(CREATE_TABLE..)
316            .is_some_and(|rest| rest.starts_with(['"', '\'']));
317    if quoted_in_the_schema {
318        return always_quoted(table);
319    }
320    quote_identifier(table)
321}
322
323/// Returns an identifier quoted, always.
324///
325/// For SQL this module *runs* rather than SQL it writes out. Nothing reads it,
326/// so there is no reason to leave a name bare and every reason not to: a
327/// reserved word is a bare word by `quote_identifier`'s rule, and leaving it
328/// bare is what made a dump lose every row of a table with a column named
329/// `"select"` (task-2066 §4.1.3).
330///
331/// @param name - the identifier
332fn always_quoted(name: &str) -> String {
333    format!("\"{}\"", name.replace('"', "\"\""))
334}
335
336/// Returns an identifier, quoted only when it has to be.
337///
338/// For the SQL this module **writes out**. A dump is read by a person as often
339/// as by a program, and quoting every name makes it noisier than the schema it
340/// came from. The rule is the usual one: a name that is a bare word needs
341/// nothing - and it is the reference's rule, asked of the pinned SQLite 3.53.4
342/// on the same schema, which emits `INSERT INTO d1 VALUES('x',1)` for a table
343/// with a column named `"select"` and quotes the table name only where the
344/// `CREATE` quoted it. `interchange.rs` compares the two byte for byte, so this
345/// is a contract rather than a preference.
346///
347/// @param name - the identifier
348fn quote_identifier(name: &str) -> String {
349    let plain = !name.is_empty()
350        && name
351            .chars()
352            .all(|character| character.is_ascii_alphanumeric() || character == '_')
353        && !name.starts_with(|character: char| character.is_ascii_digit());
354    if plain {
355        return name.to_string();
356    }
357    format!("\"{}\"", name.replace('"', "\"\""))
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    /// A bare word is left alone and anything else is quoted, with a quote
365    /// inside it doubled rather than dropped.
366    #[test]
367    fn an_identifier_is_quoted_only_when_it_has_to_be() {
368        assert_eq!(quote_identifier("t"), "t");
369        assert_eq!(quote_identifier("with_underscore"), "with_underscore");
370        assert_eq!(quote_identifier("has space"), "\"has space\"");
371        assert_eq!(quote_identifier("1leading"), "\"1leading\"");
372        assert_eq!(quote_identifier("a\"b"), "\"a\"\"b\"");
373    }
374
375    /// The name a `SELECT` is built from is always quoted.
376    ///
377    /// **Including a reserved word, which is the whole point** (task-2066
378    /// §4.1.3): `quote_identifier` leaves `select` bare because it is a bare
379    /// word, and `SELECT select,b FROM d1` does not parse - so a table with a
380    /// column of that name dumped with no rows and exit 0.
381    #[test]
382    fn a_name_a_query_is_built_from_is_always_quoted() {
383        assert_eq!(always_quoted("t"), "\"t\"");
384        assert_eq!(always_quoted("select"), "\"select\"");
385        assert_eq!(always_quoted("order"), "\"order\"");
386        assert_eq!(always_quoted(""), "\"\"");
387        assert_eq!(always_quoted("has space"), "\"has space\"");
388        assert_eq!(always_quoted("a\"b"), "\"a\"\"b\"");
389    }
390
391    /// The emitted name follows the schema's spelling, not a quoting rule.
392    ///
393    /// The reference's own behaviour, measured against the pinned SQLite
394    /// 3.53.4: a bare `CREATE TABLE d1` gives `INSERT INTO d1`, and a quoted
395    /// `CREATE TABLE "select"` gives `INSERT INTO "select"` - even though
396    /// `select` is a bare word.
397    #[test]
398    fn the_emitted_name_is_spelled_the_way_the_schema_spells_it() {
399        assert_eq!(
400            emitted_name("d1", "CREATE TABLE d1 (\"select\" TEXT, b INT)"),
401            "d1"
402        );
403        assert_eq!(
404            emitted_name("select", "CREATE TABLE \"select\"(x TEXT)"),
405            "\"select\""
406        );
407        assert_eq!(
408            emitted_name("has space", "CREATE TABLE \"has space\"(\"a b\" TEXT)"),
409            "\"has space\""
410        );
411    }
412
413    /// And the two answer differently for exactly the case that mattered.
414    ///
415    /// A test that pinned only the values above would still pass if somebody
416    /// made `always_quoted` an alias of `quote_identifier`, which is the one
417    /// change that would bring the defect back.
418    #[test]
419    fn the_two_quoting_rules_differ_on_a_reserved_word() {
420        assert_ne!(always_quoted("select"), quote_identifier("select"));
421        assert_eq!(quote_identifier("select"), "select");
422    }
423}