mod cli;
mod config;
mod db;
mod history;
mod operation;
mod tui;
use std::path::PathBuf;
use clap::Parser;
use cli::{Cli, Command, DynamoCommand, DynamoConnectionArgs};
use config::{Config, ConnectionProfile};
use db::DataSource;
use db::dynamodb::{DynamoOptions, DynamoQueryOptions, DynamoScanOptions, DynamoSource};
use db::model::QueryOutcome;
use db::sqlite::{SqliteSource, ensure_demo_db};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli.command {
Some(Command::Demo { reset }) => run_demo(reset).await?,
None => run_startup().await?,
Some(Command::Open { database, write }) => {
let source = open_source(&database, !write).await?;
tui::run(source).await?;
}
Some(Command::Profile { name }) => {
let config = Config::load()?;
let profile = config
.profile(&name)
.cloned()
.ok_or_else(|| anyhow::anyhow!("unknown connection profile `{name}`"))?;
run_profile(profile).await?;
}
Some(Command::Config { show }) => {
if show {
println!("{}", toml::to_string_pretty(&Config::load()?)?);
} else {
println!("{}", config::config_path()?.display());
}
}
Some(Command::Tables { database }) => {
let source = open_source(&database, true).await?;
println!(
"{}",
serde_json::to_string_pretty(&source.list_tables().await?)?
);
}
Some(Command::Schema { database, table }) => {
let source = open_source(&database, true).await?;
let columns = source.table_schema(&table).await?;
let output: Vec<_> = columns
.into_iter()
.map(|column| {
serde_json::json!({
"name": column.name,
"type": column.col_type,
"not_null": column.notnull,
"primary_key": column.pk,
})
})
.collect();
println!("{}", serde_json::to_string_pretty(&output)?);
}
Some(Command::Query {
database,
sql,
max_rows,
}) => {
let source = open_source(&database, true).await?;
print_outcome(source.execute_sql(&sql, max_rows).await?)?;
}
Some(Command::Execute {
database,
sql,
yes,
max_rows,
}) => {
anyhow::ensure!(yes, "refusing write-capable execution without --yes");
let source = open_source(&database, false).await?;
print_outcome(source.execute_sql(&sql, max_rows).await?)?;
}
Some(Command::Dynamodb(command)) => run_dynamodb(*command).await?,
}
Ok(())
}
async fn run_dynamodb(command: DynamoCommand) -> anyhow::Result<()> {
match command {
DynamoCommand::Open { connection, write } => {
let source = open_dynamodb(connection, !write).await?;
tui::run(DataSource::Dynamo(source)).await?;
}
DynamoCommand::Tables { connection } => {
let source = open_dynamodb(connection, true).await?;
print_json(&serde_json::json!({ "tables": source.list_tables().await? }))?;
}
DynamoCommand::Describe { table, connection } => {
let source = open_dynamodb(connection, true).await?;
print_json(&source.describe_table_json(&table).await?)?;
}
DynamoCommand::Scan {
table,
filter,
projection,
values,
names,
start_key,
limit,
consistent_read,
typed_json,
connection,
} => {
let source = open_dynamodb(connection, true).await?;
let options = DynamoScanOptions {
table,
filter,
projection,
values: values.as_deref().map(parse_json_input).transpose()?,
names: names.as_deref().map(parse_json_input).transpose()?,
start_key: start_key.as_deref().map(parse_json_input).transpose()?,
limit,
consistent_read,
typed_json,
};
print_json(&source.scan_json(options).await?)?;
}
DynamoCommand::Partiql {
statement,
parameters,
typed_json,
max_rows,
next_token,
yes,
connection,
} => {
let source = open_dynamodb(connection, !yes).await?;
let parameters = parameters.as_deref().map(parse_json_input).transpose()?;
print_outcome(
source
.execute_partiql(
&statement,
max_rows,
parameters.as_ref(),
typed_json,
next_token,
)
.await?,
)?;
}
DynamoCommand::Get {
table,
key,
projection,
names,
consistent_read,
typed_json,
connection,
} => {
let source = open_dynamodb(connection, true).await?;
let key = parse_json_input(&key)?;
let names = names.as_deref().map(parse_json_input).transpose()?;
print_json(
&source
.get_item_json(
&table,
&key,
consistent_read,
typed_json,
projection,
names.as_ref(),
)
.await?,
)?;
}
DynamoCommand::Query {
table,
key_condition,
index,
filter,
projection,
values,
names,
start_key,
limit,
consistent_read,
typed_json,
descending,
connection,
} => {
let source = open_dynamodb(connection, true).await?;
let options = DynamoQueryOptions {
table,
key_condition,
index,
filter,
projection,
values: values.as_deref().map(parse_json_input).transpose()?,
names: names.as_deref().map(parse_json_input).transpose()?,
start_key: start_key.as_deref().map(parse_json_input).transpose()?,
limit,
consistent_read,
typed_json,
scan_forward: !descending,
};
print_json(&source.query_json(options).await?)?;
}
DynamoCommand::Put {
table,
item,
condition,
values,
names,
typed_json,
yes,
connection,
} => {
anyhow::ensure!(yes, "refusing DynamoDB put without --yes");
let source = open_dynamodb(connection, false).await?;
let item = parse_json_input(&item)?;
let values = values.as_deref().map(parse_json_input).transpose()?;
let names = names.as_deref().map(parse_json_input).transpose()?;
source
.put_item_json(
&table,
&item,
typed_json,
condition,
values.as_ref(),
names.as_ref(),
)
.await?;
print_json(&serde_json::json!({ "ok": true }))?;
}
DynamoCommand::Delete {
table,
key,
condition,
values,
names,
typed_json,
yes,
connection,
} => {
anyhow::ensure!(yes, "refusing DynamoDB delete without --yes");
let source = open_dynamodb(connection, false).await?;
let key = parse_json_input(&key)?;
let values = values.as_deref().map(parse_json_input).transpose()?;
let names = names.as_deref().map(parse_json_input).transpose()?;
source
.delete_item_json(
&table,
&key,
typed_json,
condition,
values.as_ref(),
names.as_ref(),
)
.await?;
print_json(&serde_json::json!({ "ok": true }))?;
}
}
Ok(())
}
async fn run_startup() -> anyhow::Result<()> {
let mut config = Config::load()?;
match tui::startup::run(&mut config)? {
Some(tui::startup::StartupChoice::Profile(profile)) => run_profile(profile).await?,
Some(tui::startup::StartupChoice::Demo) => run_demo(false).await?,
None => {}
}
Ok(())
}
async fn run_profile(profile: ConnectionProfile) -> anyhow::Result<()> {
match profile {
ConnectionProfile::Sqlite { path, write, .. } => {
tui::run(open_source(&path, !write).await?).await?;
}
ConnectionProfile::Dynamodb {
aws_profile,
region,
endpoint_url,
local,
write,
..
} => {
let source = open_dynamodb(
DynamoConnectionArgs {
profile: aws_profile,
region,
endpoint_url,
local,
},
!write,
)
.await?;
tui::run(DataSource::Dynamo(source)).await?;
}
}
Ok(())
}
async fn open_dynamodb(
connection: DynamoConnectionArgs,
read_only: bool,
) -> anyhow::Result<DynamoSource> {
Ok(DynamoSource::connect(DynamoOptions {
profile: connection.profile,
region: connection.region,
endpoint_url: connection.endpoint_url,
read_only,
local: connection.local,
})
.await?)
}
fn parse_json_input(input: &str) -> anyhow::Result<serde_json::Value> {
let content = if input == "-" {
let mut content = String::new();
std::io::Read::read_to_string(&mut std::io::stdin(), &mut content)?;
content
} else if let Some(path) = input.strip_prefix('@') {
std::fs::read_to_string(path)?
} else {
input.to_string()
};
Ok(serde_json::from_str(&content)?)
}
fn print_json(value: &serde_json::Value) -> anyhow::Result<()> {
println!("{}", serde_json::to_string_pretty(value)?);
Ok(())
}
async fn run_demo(reset: bool) -> anyhow::Result<()> {
let path = demo_db_path()?;
if reset && path.exists() {
std::fs::remove_file(&path)?;
}
ensure_demo_db(&path).await?;
let source = DataSource::Sqlite(SqliteSource::connect(&path).await?);
tui::run(source).await?;
Ok(())
}
async fn open_source(path: &std::path::Path, read_only: bool) -> anyhow::Result<DataSource> {
anyhow::ensure!(path.exists(), "database does not exist: {}", path.display());
let source = if read_only {
SqliteSource::connect_read_only(path).await?
} else {
SqliteSource::connect(path).await?
};
Ok(DataSource::Sqlite(source))
}
fn print_outcome(outcome: QueryOutcome) -> anyhow::Result<()> {
let output = match outcome {
QueryOutcome::Rows {
columns,
rows,
truncated,
next_token,
read_only: _,
} => serde_json::json!({
"columns": columns.into_iter().map(|column| column.name).collect::<Vec<_>>(),
"rows": rows
.into_iter()
.map(|row| row.values.into_iter().map(|value| value.to_json()).collect::<Vec<_>>())
.collect::<Vec<_>>(),
"truncated": truncated,
"next_token": next_token,
}),
QueryOutcome::Affected(rows_affected) => serde_json::json!({
"rows_affected": rows_affected,
}),
QueryOutcome::Executed => serde_json::json!({ "ok": true }),
};
println!("{}", serde_json::to_string_pretty(&output)?);
Ok(())
}
fn demo_db_path() -> anyhow::Result<PathBuf> {
Ok(config::data_dir()?.join("demo.sqlite"))
}