SQLite backup
SQLite databases backed up while in use, checked for damage, and summed up by tables and rows: nightly backups you can trust.
Version 1.0.0 · 3 functions · 1 crate
Functions
What use sqlite_backup.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use sqlite_backup, sqlite_integrity from sqlite_backup (Quillo 0.6).
sqlite_backup(from: str, to: str) → anyor an errorCopies the SQLite database from to to (folders made, an old to replaced) with SQLite's online backup, a page at a time: other programs may go on reading and writing from meanwhile, and the copy is whole and consistent. { pages, bytes }. For the database this program opened with db, use db.exec("vacuum into ?", to) instead: two SQLite libraries in one process must not open one file.
sqlite_integrity(path: str) → anyor an errorSQLite's own check of a database file: { ok, problems }, problems the first 100 it finds (empty when ok). Run it on each backup.
sqlite_info(path: str) → anyor an errorA database file's summary: { bytes, page_size, pages, journal_mode, user_version, tables }, each table { name, rows }: to compare a backup with what it came from.
Get it
$ quillo install sqlite_backup # sqlite_backup.qwer into this folder$ quillo install sqlite_backup --example # and its example, sqlite_backup.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/sqlite_backup.qwer$ curl -O https://quillolang.org/plugins/files/sqlite_backup.qweThen, in your program, all of it or only what you call:
use sqlite_backup.qweruse sqlite_backup, sqlite_integrity from sqlite_backup // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
rusqlite = { version = "0.32", features = ["bundled", "backup"] }Example
// A nightly backup of the shop's database: a consistent copy, checked, summed up, and a second copy for the off-site disk.//// quillo run sqlite_backup.qwe then open http://localhost:8080/backupuse "sqlite_backup.qwer"db.open("data/shop.db")db.exec("create table if not exists orders (id integer primary key, customer text, total real)")db.exec("create table if not exists customers (id integer primary key, name text)")route GET /backup { db.exec("delete from orders") for i in range(250) { db.exec("insert into orders (customer, total) values (?, ?)", `customer ${i % 40}`, i * 1.5) } // The program's own database is copied by its own SQLite... write_file("data/backups/.keep", "") // vacuum into makes no folders delete_file("data/backups/shop.db") // nor replaces a file db.exec("vacuum into ?", "data/backups/shop.db")? // ...and the copy, which nothing has open, checked and copied again. { check: sqlite_integrity("data/backups/shop.db")?, info: sqlite_info("data/backups/shop.db")?, offsite: sqlite_backup("data/backups/shop.db", "data/offsite/shop.db")?, }}$ quillo run sqlite_backup.qwe$ curl 'localhost:8080/backup'Source
//! SQLite databases backed up while in use, checked for damage, and summed up (tables and rows): for nightly backups you can trust.//! version: 1.0.0//!//! [dependencies]//! rusqlite = { version = "0.32", features = ["bundled", "backup"] }use rusqlite::{Connection, OpenFlags};use serde_json::{json, Value};fn open_ro(path: &str) -> Result<Connection, String> { if !std::path::Path::new(path).is_file() { return Err(format!("{}: no such database file", path)); } Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX).map_err(|e| format!("{}: {}", path, e))}/// Copies the SQLite database `from` to `to` (folders made, an old `to` replaced) with SQLite's online backup, a page at a time:/// other programs may go on reading and writing `from` meanwhile, and the copy is whole and consistent. `{ pages, bytes }`./// For the database this program opened with `db`, use `db.exec("vacuum into ?", to)` instead: two SQLite libraries in one process must not open one file.pub fn sqlite_backup(from: &str, to: &str) -> Result<Value, String> { let src = open_ro(from)?; if let Some(dir) = std::path::Path::new(to).parent().filter(|d| !d.as_os_str().is_empty()) { std::fs::create_dir_all(dir).map_err(|e| format!("{}: {}", dir.display(), e))?; } let tmp = format!("{}.part", to); let _ = std::fs::remove_file(&tmp); { let mut dst = Connection::open(&tmp).map_err(|e| format!("{}: {}", tmp, e))?; let b = rusqlite::backup::Backup::new(&src, &mut dst).map_err(|e| e.to_string())?; b.run_to_completion(256, std::time::Duration::from_millis(5), None).map_err(|e| format!("backup of {}: {}", from, e))?; } std::fs::rename(&tmp, to).map_err(|e| format!("{}: {}", to, e))?; let pages: i64 = open_ro(to)?.query_row("pragma page_count", [], |r| r.get(0)).map_err(|e| e.to_string())?; Ok(json!({ "pages": pages, "bytes": std::fs::metadata(to).map_err(|e| e.to_string())?.len() }))}/// SQLite's own check of a database file: `{ ok, problems }`, `problems` the first 100 it finds (empty when ok). Run it on each backup.pub fn sqlite_integrity(path: &str) -> Result<Value, String> { let c = open_ro(path)?; let mut st = c.prepare("pragma integrity_check(100)").map_err(|e| format!("{}: {}", path, e))?; let rows: Vec<String> = st.query_map([], |r| r.get::<_, String>(0)).map_err(|e| format!("{}: {}", path, e))?.filter_map(Result::ok).collect(); let ok = rows.len() == 1 && rows[0] == "ok"; Ok(json!({ "ok": ok, "problems": if ok { vec![] } else { rows } }))}/// A database file's summary: `{ bytes, page_size, pages, journal_mode, user_version, tables }`, each table `{ name, rows }`: to compare a backup with what it came from.pub fn sqlite_info(path: &str) -> Result<Value, String> { let c = open_ro(path)?; let e = |e: rusqlite::Error| format!("{}: {}", path, e); let one = |sql: &str| -> Result<Value, String> { c.query_row(sql, [], |r| r.get::<_, rusqlite::types::Value>(0)).map(|v| match v { rusqlite::types::Value::Integer(i) => Value::from(i), rusqlite::types::Value::Text(s) => Value::from(s), _ => Value::Null, }).map_err(e) }; let mut st = c.prepare("select name from sqlite_schema where type = 'table' and name not like 'sqlite_%' order by name").map_err(e)?; let names: Vec<String> = st.query_map([], |r| r.get(0)).map_err(e)?.filter_map(Result::ok).collect(); let mut tables = vec![]; for n in names.iter().take(1000) { let rows: i64 = c.query_row(&format!("select count(*) from \"{}\"", n.replace('"', "\"\"")), [], |r| r.get(0)).map_err(e)?; tables.push(json!({ "name": n, "rows": rows })); } Ok(json!({ "bytes": std::fs::metadata(path).map_err(|e| e.to_string())?.len(), "page_size": one("pragma page_size")?, "pages": one("pragma page_count")?, "journal_mode": one("pragma journal_mode")?, "user_version": one("pragma user_version")?, "tables": tables, }))}Related
bzip2.qwer · 1.0.0bzip2 compression of files and text in pure Rust, .bz2 dumps unpacked, and their first lines read without unpacking them all.6 views · 7 downloadsDisk space disk_space.qwer · 1.0.0Free and total space of the disk a path is on, for health checks, refusing uploads before the disk fills, and admin pages.8 views · 8 downloadsFile tree file_tree.qwer · 1.0.0A folder's files, recursively, with sizes and dates: listings with limits, a folder's total size, and its largest and newest files.9 views · 6 downloadsTar tar.qwer · 1.0.0Tar archives, gzipped or not: made from files and folders, listed, one file read, and opened safely into a folder.7 views · 8 downloadsxz xz.qwer · 1.0.0xz (LZMA2) compression of files and text, and old .lzma files read: the smallest archives, for backups and downloads.6 views · 8 downloadsZstandard zstd.qwer · 1.0.0Zstandard compression of files and text: faster than gzip at the same size, for backups, logs and caches.8 views · 6 downloadsThread 0 posts
On SQLite backup: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.