use inillucent_value::Value;
use crate::render::literal;
use crate::shell::Shell;
pub fn dump(shell: &mut Shell, pattern: Option<&str>) {
let tables = table_objects(shell, pattern);
if tables
.iter()
.any(|(_, sql)| sql.starts_with("CREATE VIRTUAL TABLE"))
{
shell.say("/* WARNING: Script requires that SQLITE_DBCONFIG_DEFENSIVE be disabled */");
}
shell.say("PRAGMA foreign_keys=OFF;");
shell.say("BEGIN TRANSACTION;");
let mut writable = false;
for (name, sql) in &tables {
say_definition(shell, name, sql, &mut writable);
if sql.starts_with("CREATE VIRTUAL TABLE") {
continue;
}
write_rows(shell, name, sql);
}
for sql in other_objects(shell, pattern) {
shell.say(&format!("{sql};"));
}
if writable {
shell.say("PRAGMA writable_schema=OFF;");
}
shell.say("COMMIT;");
}
fn table_objects(shell: &Shell, pattern: Option<&str>) -> Vec<(String, String)> {
let mut sql = String::from(
"SELECT name, sql FROM sqlite_master WHERE type = 'table' AND sql IS NOT NULL",
);
if let Some(pattern) = pattern {
let quoted = format!("'{}'", pattern.replace('\'', "''"));
sql.push_str(&format!(
" AND (name LIKE {quoted} OR tbl_name LIKE {quoted})"
));
}
sql.push_str(" ORDER BY tbl_name = 'sqlite_sequence', rowid");
let Ok((_, rows)) = shell.collect(&sql) else {
return Vec::new();
};
rows.iter()
.map(|row| (plain(row.first()), plain(row.get(1))))
.collect()
}
fn other_objects(shell: &Shell, pattern: Option<&str>) -> Vec<String> {
let mut sql = String::from(
"SELECT sql FROM sqlite_master \
WHERE sql IS NOT NULL AND type IN ('index', 'trigger', 'view')",
);
if let Some(pattern) = pattern {
let quoted = format!("'{}'", pattern.replace('\'', "''"));
sql.push_str(&format!(
" AND (name LIKE {quoted} OR tbl_name LIKE {quoted})"
));
}
sql.push_str(" ORDER BY type DESC, rowid");
let Ok((_, rows)) = shell.collect(&sql) else {
return Vec::new();
};
rows.iter().map(|row| plain(row.first())).collect()
}
fn plain(value: Option<&Value<'static>>) -> String {
match value {
Some(Value::Text(text)) => String::from_utf8_lossy(text.raw()).into_owned(),
Some(Value::Null) | None => String::new(),
Some(other) => literal(other),
}
}
fn say_definition(shell: &mut Shell, name: &str, sql: &str, writable: &mut bool) {
const CREATE_TABLE: usize = 13;
if is_statistics_table(name) {
shell.say("ANALYZE sqlite_schema;");
return;
}
if name.len() >= 7 && name[..7].eq_ignore_ascii_case("sqlite_") {
make_writable(shell, writable);
if let Some(rest) = sql.get(CREATE_TABLE..) {
shell.say(&format!("CREATE TABLE IF NOT EXISTS {rest};"));
}
if name.eq_ignore_ascii_case("sqlite_sequence") {
shell.say("DELETE FROM sqlite_sequence;");
}
return;
}
if sql.starts_with("CREATE VIRTUAL TABLE") {
make_writable(shell, writable);
let escaped_name = name.replace('\'', "''");
let escaped_sql = sql.replace('\'', "''");
shell.say(&format!(
"INSERT INTO sqlite_schema(type,name,tbl_name,rootpage,sql)VALUES('table','{escaped_name}','{escaped_name}',0,'{escaped_sql}');"
));
return;
}
let quoted_name = sql.len() > CREATE_TABLE
&& sql[..CREATE_TABLE].eq_ignore_ascii_case("CREATE TABLE ")
&& sql[CREATE_TABLE..].starts_with(['"', '\'']);
if quoted_name {
shell.say(&format!(
"CREATE TABLE IF NOT EXISTS {};",
&sql[CREATE_TABLE..]
));
return;
}
shell.say(&format!("{sql};"));
}
fn make_writable(shell: &mut Shell, writable: &mut bool) {
if *writable {
return;
}
shell.say("PRAGMA writable_schema=ON;");
*writable = true;
}
fn is_statistics_table(name: &str) -> bool {
name.len() == 12 && name[..11].eq_ignore_ascii_case("sqlite_stat")
}
fn stored_columns(shell: &mut Shell, table: &str) -> Vec<String> {
let literal = format!("'{}'", table.replace('\'', "''"));
let Ok((_, rows)) = shell.collect(&format!("SELECT name FROM pragma_table_info({literal})"))
else {
return Vec::new();
};
rows.iter().map(|row| plain(row.first())).collect()
}
fn write_rows(shell: &mut Shell, table: &str, sql: &str) {
let quoted = emitted_name(table, sql);
let columns = stored_columns(shell, table);
let projection = if columns.is_empty() {
"*".to_string()
} else {
columns
.iter()
.map(|name| always_quoted(name))
.collect::<Vec<String>>()
.join(",")
};
let reading = format!("SELECT {projection} FROM {}", always_quoted(table));
let Ok((_, rows)) = shell.collect(&reading) else {
shell.complain(&format!(
"-- the rows of {table} could not be read, so none are in this dump"
));
return;
};
for row in rows {
let values: Vec<String> = row.iter().map(literal).collect();
shell.say(&format!(
"INSERT INTO {quoted} VALUES({});",
values.join(",")
));
}
}
fn emitted_name(table: &str, sql: &str) -> String {
const CREATE_TABLE: usize = 13;
let quoted_in_the_schema = sql.len() > CREATE_TABLE
&& sql
.get(..CREATE_TABLE)
.is_some_and(|head| head.eq_ignore_ascii_case("CREATE TABLE "))
&& sql
.get(CREATE_TABLE..)
.is_some_and(|rest| rest.starts_with(['"', '\'']));
if quoted_in_the_schema {
return always_quoted(table);
}
quote_identifier(table)
}
fn always_quoted(name: &str) -> String {
format!("\"{}\"", name.replace('"', "\"\""))
}
fn quote_identifier(name: &str) -> String {
let plain = !name.is_empty()
&& name
.chars()
.all(|character| character.is_ascii_alphanumeric() || character == '_')
&& !name.starts_with(|character: char| character.is_ascii_digit());
if plain {
return name.to_string();
}
format!("\"{}\"", name.replace('"', "\"\""))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_identifier_is_quoted_only_when_it_has_to_be() {
assert_eq!(quote_identifier("t"), "t");
assert_eq!(quote_identifier("with_underscore"), "with_underscore");
assert_eq!(quote_identifier("has space"), "\"has space\"");
assert_eq!(quote_identifier("1leading"), "\"1leading\"");
assert_eq!(quote_identifier("a\"b"), "\"a\"\"b\"");
}
#[test]
fn a_name_a_query_is_built_from_is_always_quoted() {
assert_eq!(always_quoted("t"), "\"t\"");
assert_eq!(always_quoted("select"), "\"select\"");
assert_eq!(always_quoted("order"), "\"order\"");
assert_eq!(always_quoted(""), "\"\"");
assert_eq!(always_quoted("has space"), "\"has space\"");
assert_eq!(always_quoted("a\"b"), "\"a\"\"b\"");
}
#[test]
fn the_emitted_name_is_spelled_the_way_the_schema_spells_it() {
assert_eq!(
emitted_name("d1", "CREATE TABLE d1 (\"select\" TEXT, b INT)"),
"d1"
);
assert_eq!(
emitted_name("select", "CREATE TABLE \"select\"(x TEXT)"),
"\"select\""
);
assert_eq!(
emitted_name("has space", "CREATE TABLE \"has space\"(\"a b\" TEXT)"),
"\"has space\""
);
}
#[test]
fn the_two_quoting_rules_differ_on_a_reserved_word() {
assert_ne!(always_quoted("select"), quote_identifier("select"));
assert_eq!(quote_identifier("select"), "select");
}
}