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}