feat: add read-only SQLite inspection

This commit is contained in:
Valdemar Lorenzen
2026-10-19 12:06:25 +02:00
parent c73fa18bf3
commit bbc21f96cf
6 changed files with 460 additions and 43 deletions
+38 -17
View File
@@ -1,14 +1,15 @@
mod app;
mod db;
use std::{error::Error, io::IsTerminal, process::ExitCode};
use std::{error::Error, io::IsTerminal, path::Path, process::ExitCode};
type Result<T, E = Box<dyn Error>> = std::result::Result<T, E>;
const HELP: &str = r#"dbdash — read-only PostgreSQL inspection
const HELP: &str = r#"dbdash — read-only PostgreSQL and SQLite inspection
Usage:
dbdash [POSTGRES_CONNECTION_STRING]
dbdash --sqlite PATH
dbdash --help
With no argument, the connection is read from DATABASE_URL.
@@ -16,6 +17,10 @@ Prefer the environment variable to keep passwords out of process arguments:
export DATABASE_URL='postgresql://user@localhost:5432/database'
cargo run
Open an existing SQLite database file (read-only; missing files are not created):
cargo run -- --sqlite ./database.sqlite
SQLite paths can be relative or absolute; quote paths containing spaces.
PostgreSQL URLs and libpq-style key=value strings are accepted.
Use sslmode=require for mandatory TLS; certificates and hostnames are verified.
Use sslmode=disable only for trusted local connections.
@@ -31,9 +36,11 @@ Navigation:
r Refresh relations and the active relation
q / Ctrl-C Quit
Reads are enforced read-only by PostgreSQL and have a 5-second query timeout.
The interface waits for reads to complete. NULL cells are dimmed.
Pages use primary-key order, or physical order for keyless tables.
PostgreSQL reads are enforced read-only and have a 5-second query timeout.
SQLite files are opened read-only, with a 5-second lock wait but no query timeout.
The interface waits for reads to complete. NULL cells are dimmed; SQLite BLOBs
are shown as X'hex'. Only SQLite's main database is inspected.
Pages use primary-key order, or physical row order for keyless tables when available.
Keyless views have no guaranteed order; concurrent changes can shift pages.
"#;
@@ -49,24 +56,38 @@ fn main() -> ExitCode {
fn run() -> Result<()> {
let mut args = std::env::args_os().skip(1);
let connection = match args.next() {
Some(argument) if argument == "--help" || argument == "-h" => {
print!("{HELP}");
return Ok(());
}
Some(argument) => argument
.into_string()
.map_err(|_| "connection string must be UTF-8")?,
None => std::env::var("DATABASE_URL")
.map_err(|_| "set DATABASE_URL or pass a connection string; see dbdash --help")?,
let argument = args.next();
if argument
.as_ref()
.is_some_and(|arg| arg == "--help" || arg == "-h")
{
print!("{HELP}");
return Ok(());
}
let sqlite = argument.as_ref().is_some_and(|arg| arg == "--sqlite");
let connection = if sqlite {
args.next()
.ok_or("--sqlite requires a database file path; see dbdash --help")?
} else {
argument.or_else(|| std::env::var_os("DATABASE_URL"))
.ok_or("set DATABASE_URL, pass a PostgreSQL connection string, or use --sqlite PATH; see dbdash --help")?
};
if args.next().is_some() {
return Err("expected one connection string; see dbdash --help".into());
return Err("unexpected extra arguments; see dbdash --help".into());
}
if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() {
return Err("an interactive terminal is required; see dbdash --help".into());
}
let mut app = app::App::new(db::Database::connect(&connection)?)?;
let database = if sqlite {
db::Database::open_sqlite(Path::new(&connection))?
} else {
db::Database::connect(
connection
.to_str()
.ok_or("connection string must be UTF-8")?,
)?
};
let mut app = app::App::new(database)?;
let result = (|| {
let mut terminal = ratatui::try_init()?;
app.run(&mut terminal)