Basic dump support

This is a basic support for the very useful .dump command.
It doesn't yet implement any of the dump options sqlite has,
and it doesn't add some of the logic for things like indexes,
since we don't have them.
This commit is contained in:
Glauber Costa 2025-01-29 20:07:50 -05:00
parent d9f4558255
commit 82ceaebe01

View file

@ -133,6 +133,8 @@ pub enum Command {
Import,
/// Loads an extension library
LoadExtension,
/// Dump the current database as a list of SQL statements
Dump,
}
impl Command {
@ -145,7 +147,8 @@ impl Command {
| Self::Opcodes
| Self::ShowInfo
| Self::Tables
| Self::SetOutput => 0,
| Self::SetOutput
| Self::Dump => 0,
Self::Open
| Self::OutputMode
| Self::Cwd
@ -172,6 +175,7 @@ impl Command {
Self::Echo => ".echo on|off",
Self::Tables => ".tables",
Self::LoadExtension => ".load",
Self::Dump => ".dump",
Self::Import => &IMPORT_HELP,
}
}
@ -196,6 +200,7 @@ impl FromStr for Command {
".echo" => Ok(Self::Echo),
".import" => Ok(Self::Import),
".load" => Ok(Self::LoadExtension),
".dump" => Ok(Self::Dump),
_ => Err("Unknown command".to_string()),
}
}
@ -261,6 +266,31 @@ impl std::fmt::Display for Settings {
}
}
macro_rules! query_internal {
($self:expr, $query:expr, $body:expr) => {{
let rows = $self.conn.query($query)?;
if let Some(mut rows) = rows {
loop {
match rows.step()? {
StepResult::Row => {
let row = rows.row().unwrap();
$body(row)?;
}
StepResult::IO => {
$self.io.run_once()?;
}
StepResult::Interrupt => break,
StepResult::Done => break,
StepResult::Busy => {
Err(LimboError::InternalError("database is busy".into()))?;
}
}
}
}
Ok::<(), LimboError>(())
}};
}
impl Limbo {
pub fn new() -> anyhow::Result<Self> {
let opts = Opts::parse();
@ -336,6 +366,80 @@ impl Limbo {
.map_err(|e| e.to_string())
}
fn dump_table(&mut self, name: &str) -> Result<(), LimboError> {
let query = format!("pragma table_info={}", name);
let mut cols = vec![];
query_internal!(
self,
query,
|row: &limbo_core::Row| -> Result<(), LimboError> {
let name: &str = row.get::<&str>(1)?;
cols.push(name.to_string());
Ok(())
}
)?;
// FIXME: sqlite has logic to check rowid and optionally preserve
// it, but it requires pragma index_list, and it seems to be relevant
// only for indexes.
let cols_str = cols.join(", ");
let select = format!("select {} from {}", cols_str, name);
query_internal!(
self,
select,
|row: &limbo_core::Row| -> Result<(), LimboError> {
let values = row
.get_values()
.into_iter()
.map(|x| x.to_string())
.collect::<Vec<_>>()
.join(",");
let _ = self.write_fmt(format_args!("INSERT INTO {} VALUES({});", name, values))?;
Ok(())
}
)?;
Ok(())
}
fn dump_database(&mut self) -> anyhow::Result<()> {
self.writeln("PRAGMA foreign_keys=OFF;")?;
self.writeln("BEGIN TRANSACTION;")?;
// FIXME: At this point, SQLite executes the following:
// sqlite3_exec(p->db, "SAVEPOINT dump; PRAGMA writable_schema=ON", 0, 0, 0);
// we don't have those yet, so don't.
let query = r#"
SELECT name, type, sql
FROM sqlite_schema AS o
WHERE type == 'table'
AND sql NOT NULL
ORDER BY tbl_name = 'sqlite_sequence', rowid"#;
let res = query_internal!(
self,
query,
|row: &limbo_core::Row| -> Result<(), LimboError> {
let sql: &str = row.get::<&str>(2)?;
let name: &str = row.get::<&str>(0)?;
let _ = self.write_fmt(format_args!("{};", sql))?;
self.dump_table(name)
}
);
match res {
Ok(_) => Ok(()),
Err(LimboError::Corrupt(x)) => {
// FIXME: SQLite at this point retry the query with a different
// order by, but for simplicity we are just ignoring for now
self.writeln("/****** CORRUPTION ERROR *******/")?;
Err(LimboError::Corrupt(x))
}
Err(x) => Err(x),
}?;
self.conn.close()?;
self.writeln("COMMIT;")?;
Ok(())
}
fn display_in_memory(&mut self) -> io::Result<()> {
if self.opts.db_file == ":memory:" {
self.writeln("Connected to a transient in-memory database.")?;
@ -599,6 +703,11 @@ impl Limbo {
let _ = self.writeln(&e);
}
}
Command::Dump => {
if let Err(e) = self.dump_database() {
let _ = self.write_fmt(format_args!("/****** ERROR: {} ******/", e));
}
}
}
} else {
let _ = self.write_fmt(format_args!(