feat: support explicit read-write database connections
This commit is contained in:
+55
-23
@@ -12,12 +12,12 @@ use crossterm::event::{
|
||||
|
||||
type Result<T, E = Box<dyn Error>> = std::result::Result<T, E>;
|
||||
|
||||
const HELP: &str = r#"dbdash — read-only PostgreSQL and SQLite inspection
|
||||
const HELP: &str = r#"dbdash — PostgreSQL and SQLite inspection with opt-in SQL writes
|
||||
|
||||
Usage:
|
||||
dbdash [POSTGRES_CONNECTION_STRING]
|
||||
dbdash --ca-bundle PATH POSTGRES_CONNECTION_STRING
|
||||
dbdash --sqlite PATH
|
||||
dbdash [--read-write] [POSTGRES_CONNECTION_STRING]
|
||||
dbdash [--read-write] --ca-bundle PATH POSTGRES_CONNECTION_STRING
|
||||
dbdash [--read-write] --sqlite PATH
|
||||
dbdash --help
|
||||
|
||||
Run without a connection argument to open the saved database list:
|
||||
@@ -30,6 +30,14 @@ SQLite paths can be relative or absolute; quote paths containing spaces.
|
||||
SQLite paths expand leading ~, $VAR, and ${VAR}; undefined variables are errors.
|
||||
Expansion never runs shell commands. Quote CLI paths to let dbdash expand them.
|
||||
|
||||
Connections default to READ ONLY, including existing saved configurations.
|
||||
Choose Access in the connection window to save READ ONLY or READ WRITE per database.
|
||||
For an unsaved CLI session, opt in explicitly:
|
||||
cargo run -- --read-write --sqlite ./database.sqlite
|
||||
--read-write requires a direct connection; it does not change saved profiles.
|
||||
The header and connected sidebar badge reflect the live session, not saved edits.
|
||||
Changing saved Access requires reconnecting (R on the saved database or a child).
|
||||
|
||||
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.
|
||||
@@ -68,12 +76,13 @@ Navigation:
|
||||
i Show configured database info (Esc/Enter closes; passwords omitted)
|
||||
Works on a saved database entry or any of its table/view children.
|
||||
r Refresh relations and the active relation
|
||||
Connected databases have a trailing * and show their tables/views beneath them.
|
||||
Databases always show [RO]/[RW], even when their labels truncate; connected ones
|
||||
have a trailing * and show their tables/views beneath them. R reconnects a saved database.
|
||||
Unnamed SQLite entries show only the filename; the info window shows the full path.
|
||||
Enter on the connected database collapses/expands it without reconnecting.
|
||||
Keybindings float above the workspace; Up/Down/jk scroll, Home/End/g/G jump.
|
||||
Query Insert mode and connection/password fields keep ? as literal text.
|
||||
Full-width bars show dbdash/READ ONLY above and mode/database below.
|
||||
Full-width bars show dbdash/live READ ONLY or READ WRITE above and mode/database below.
|
||||
Saved PostgreSQL connections show database · user@host:port beside the mode,
|
||||
regardless of custom sidebar names; passwords are never included.
|
||||
The database provider sits immediately left of the bottom-right position badge.
|
||||
@@ -94,23 +103,29 @@ Navigation:
|
||||
Connection/password fields remain direct-entry; query paste requires INSERT.
|
||||
SQL syntax colors follow the connected database's PostgreSQL/SQLite dialect,
|
||||
including quoted identifiers, literals, and multiline comments/strings as you type.
|
||||
F5 / Ctrl-Enter executes one SELECT/CTE on the current connection, showing up
|
||||
to 100 rows below. Results report truncation; use SQL LIMIT/OFFSET for more.
|
||||
F5 / Ctrl-Enter executes one SQL statement on the current connection, showing up
|
||||
to 100 result rows below. Results report truncation; use SQL LIMIT/OFFSET for more.
|
||||
READ ONLY accepts SELECT/CTE reads; READ WRITE also supports SQL writes.
|
||||
Each successful statement commits atomically; failures roll back. Explicit transaction
|
||||
control and multi-statement scripts are rejected. Writes report affected-row counts;
|
||||
RETURNING displays result rows (up to 100) without limiting the rows written.
|
||||
Cells are never edited inline: use INSERT, UPDATE, DELETE, etc. in the SQL editor.
|
||||
Ctrl-Enter needs terminal support for enhanced keyboard reporting; use F5 otherwise.
|
||||
Failed queries keep the previous results. Opening a relation restores browsing
|
||||
without clearing the query text. Query text is not saved across connections.
|
||||
q / Ctrl-C Quit outside the query editor (q inserts text only in INSERT)
|
||||
New database window: Up/Down chooses a provider; Enter starts editing.
|
||||
Tab/Shift-Tab changes fields; Left/Right changes TLS or the CA bundle when focused.
|
||||
Tab/Shift-Tab changes fields; Left/Right changes TLS, CA bundle, or Access when focused.
|
||||
Enter saves without connecting, including when the database is offline.
|
||||
Optional Name controls the sidebar label; leave it blank for the default label.
|
||||
e on a saved database or its child edits the configuration without connecting.
|
||||
Enter saves edits; Esc discards them. Connected sessions keep their actual identity.
|
||||
Enter saves edits; Esc discards them. Connected sessions keep their actual identity
|
||||
and access mode until R reconnects; Enter on an active database still only expands it.
|
||||
d / Delete on a saved database asks for y to remove its configuration (n/Esc cancels).
|
||||
Database files are never deleted. Removing the active profile keeps its session open.
|
||||
Select a saved database and press Enter to connect. PostgreSQL opens an editable
|
||||
connection window, initially focused on the password. Tab/Shift-Tab changes fields;
|
||||
Left/Right changes TLS or the CA bundle when focused. Enter connects and updates that saved entry
|
||||
Left/Right changes TLS, CA bundle, or Access when focused. Enter connects and updates that saved entry
|
||||
only on success; failures keep the previous settings and connection. Esc discards edits.
|
||||
Passwords are masked, never saved, and clear on success or cancellation.
|
||||
Owned input buffers are zeroized on clear/drop; profile saving does not copy passwords.
|
||||
@@ -121,10 +136,12 @@ Navigation:
|
||||
back to ~/.config/dbdash/connections.sqlite3. SQLite paths are saved absolute.
|
||||
CLI connections are not saved. Failed switches keep the current database open.
|
||||
|
||||
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.
|
||||
PostgreSQL statements have a 5-second timeout. READ ONLY sessions enforce read-only
|
||||
transactions; READ WRITE statements commit on success. SQLite READ ONLY opens files
|
||||
read-only; READ WRITE permits writes to existing files. Missing files are never created.
|
||||
SQLite has a 5-second lock wait but no query timeout. The interface waits for statements
|
||||
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.
|
||||
"#;
|
||||
@@ -152,13 +169,27 @@ fn run() -> Result<()> {
|
||||
if std::env::var_os("PGSSLROOTCERT").is_some() {
|
||||
return Err("Unset PGSSLROOTCERT and select a CA bundle per connection, or use --ca-bundle PATH on the CLI.".into());
|
||||
}
|
||||
let root_cert = if argument.as_ref().is_some_and(|arg| arg == "--ca-bundle") {
|
||||
let path = args.next().ok_or("--ca-bundle requires a PEM file path")?;
|
||||
argument = Some(args.next().ok_or("--ca-bundle requires a PostgreSQL connection string")?);
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut access = db::AccessMode::ReadOnly;
|
||||
let mut root_cert = None;
|
||||
loop {
|
||||
match argument.as_deref().and_then(|arg| arg.to_str()) {
|
||||
Some("--read-write") => access = db::AccessMode::ReadWrite,
|
||||
Some("--ca-bundle") => {
|
||||
if root_cert.is_some() {
|
||||
return Err("--ca-bundle may only be specified once".into());
|
||||
}
|
||||
root_cert = Some(args.next().ok_or("--ca-bundle requires a PEM file path")?);
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
argument = args.next();
|
||||
}
|
||||
if root_cert.is_some() && argument.is_none() {
|
||||
return Err("--ca-bundle requires a PostgreSQL connection string".into());
|
||||
}
|
||||
if access == db::AccessMode::ReadWrite && argument.is_none() {
|
||||
return Err("--read-write requires a direct database connection; change saved Access in the connection window".into());
|
||||
}
|
||||
let sqlite = argument.as_ref().is_some_and(|arg| arg == "--sqlite");
|
||||
if sqlite && root_cert.is_some() {
|
||||
return Err("--ca-bundle is only supported for PostgreSQL".into());
|
||||
@@ -180,13 +211,14 @@ fn run() -> Result<()> {
|
||||
let database = connection
|
||||
.map(|connection| {
|
||||
if sqlite {
|
||||
db::Database::open_sqlite(Path::new(&connection))
|
||||
db::Database::open_sqlite(Path::new(&connection), access)
|
||||
} else {
|
||||
db::Database::connect(
|
||||
connection
|
||||
.to_str()
|
||||
.ok_or("connection string must be UTF-8")?,
|
||||
root_cert.as_deref().map(Path::new),
|
||||
access,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user