cli: Add tool in case `sqlite3` is unavailable

Adds the `rad node db exec <query>` command.

Easy way to run sqlite queries if the sqlite3 CLI is not available or
too old.
This commit is contained in:
cloudhead 2024-04-06 17:55:43 +02:00
parent ad7ba82e6a
commit bd8e0ebcda
No known key found for this signature in database
3 changed files with 73 additions and 0 deletions

View File

@ -13,6 +13,8 @@ use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help}; use crate::terminal::args::{Args, Error, Help};
use crate::terminal::Element as _; use crate::terminal::Element as _;
#[path = "node/commands.rs"]
mod commands;
#[path = "node/control.rs"] #[path = "node/control.rs"]
pub mod control; pub mod control;
#[path = "node/events.rs"] #[path = "node/events.rs"]
@ -35,6 +37,7 @@ Usage
rad node routing [--rid <rid>] [--nid <nid>] [--json] [<option>...] rad node routing [--rid <rid>] [--nid <nid>] [--json] [<option>...]
rad node events [--timeout <secs>] [-n <count>] [<option>...] rad node events [--timeout <secs>] [-n <count>] [<option>...]
rad node config [--addresses] rad node config [--addresses]
rad node db <command> [<option>..]
For `<node-option>` see `radicle-node --help`. For `<node-option>` see `radicle-node --help`.
@ -73,6 +76,9 @@ pub enum Operation {
Config { Config {
addresses: bool, addresses: bool,
}, },
Db {
args: Vec<OsString>,
},
Events { Events {
timeout: time::Duration, timeout: time::Duration,
count: usize, count: usize,
@ -100,6 +106,7 @@ pub enum Operation {
pub enum OperationName { pub enum OperationName {
Connect, Connect,
Config, Config,
Db,
Events, Events,
Routing, Routing,
Logs, Logs,
@ -136,6 +143,7 @@ impl Args for Options {
} }
Value(val) if op.is_none() => match val.to_string_lossy().as_ref() { Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
"connect" => op = Some(OperationName::Connect), "connect" => op = Some(OperationName::Connect),
"db" => op = Some(OperationName::Db),
"events" => op = Some(OperationName::Events), "events" => op = Some(OperationName::Events),
"logs" => op = Some(OperationName::Logs), "logs" => op = Some(OperationName::Logs),
"config" => op = Some(OperationName::Config), "config" => op = Some(OperationName::Config),
@ -188,6 +196,9 @@ impl Args for Options {
Value(val) if matches!(op, Some(OperationName::Start)) => { Value(val) if matches!(op, Some(OperationName::Start)) => {
options.push(val); options.push(val);
} }
Value(val) if matches!(op, Some(OperationName::Db)) => {
options.push(val);
}
_ => return Err(anyhow!(arg.unexpected())), _ => return Err(anyhow!(arg.unexpected())),
} }
} }
@ -200,6 +211,7 @@ impl Args for Options {
timeout, timeout,
}, },
OperationName::Config => Operation::Config { addresses }, OperationName::Config => Operation::Config { addresses },
OperationName::Db => Operation::Db { args: options },
OperationName::Events => Operation::Events { timeout, count }, OperationName::Events => Operation::Events { timeout, count },
OperationName::Routing => Operation::Routing { rid, nid, json }, OperationName::Routing => Operation::Routing { rid, nid, json },
OperationName::Logs => Operation::Logs { lines }, OperationName::Logs => Operation::Logs { lines },
@ -235,6 +247,9 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
control::config(&node)?; control::config(&node)?;
} }
} }
Operation::Db { args } => {
commands::db(&profile, args)?;
}
Operation::Sessions => { Operation::Sessions => {
let sessions = control::sessions(&node)?; let sessions = control::sessions(&node)?;
if let Some(table) = sessions { if let Some(table) = sessions {

View File

@ -0,0 +1,49 @@
use std::ffi::OsString;
use anyhow::anyhow;
use radicle::Profile;
use radicle_term as term;
#[derive(PartialEq, Eq)]
pub enum Operation {
Exec { query: String },
}
pub fn db(profile: &Profile, args: Vec<OsString>) -> anyhow::Result<()> {
use lexopt::prelude::*;
let mut parser = lexopt::Parser::from_args(args);
let mut op: Option<Operation> = None;
while let Some(arg) = parser.next()? {
match arg {
Value(cmd) if op.is_none() => match cmd.to_string_lossy().as_ref() {
"exec" => {
let val = parser
.value()
.map_err(|_| anyhow!("a query to execute must be provided for `exec`"))?;
op = Some(Operation::Exec {
query: val.to_string_lossy().to_string(),
});
}
unknown => anyhow::bail!("unknown operation '{unknown}'"),
},
_ => return Err(anyhow!(arg.unexpected())),
}
}
match op.ok_or_else(|| anyhow!("a command must be provided, eg. `rad node db exec`"))? {
Operation::Exec { query } => {
let db = profile.database_mut()?;
db.execute(query)?;
let changed = db.change_count();
if changed > 0 {
term::success!("{changed} row(s) affected.");
} else {
term::print(term::format::italic("No rows affected."));
}
}
}
Ok(())
}

View File

@ -7,6 +7,7 @@
//! //!
//! The database schema is contained within the first migration. See [`version`], [`bump`] and //! The database schema is contained within the first migration. See [`version`], [`bump`] and
//! [`migrate`] for how this works. //! [`migrate`] for how this works.
use std::ops::Deref;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use std::{fmt, time}; use std::{fmt, time};
@ -45,6 +46,14 @@ pub struct Database {
pub db: Arc<sql::ConnectionThreadSafe>, pub db: Arc<sql::ConnectionThreadSafe>,
} }
impl Deref for Database {
type Target = sql::ConnectionThreadSafe;
fn deref(&self) -> &Self::Target {
&self.db
}
}
impl fmt::Debug for Database { impl fmt::Debug for Database {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Database").finish() f.debug_struct("Database").finish()