use super::{ClassifyError, ShellContext, Sink};
use crate::paths;
const DATABASE_SHELLS: &[&str] = &["sqlite3", "sqlite", "duckdb"];
const SQL_VALUED: &[&str] = &["cmd", "c", "s"];
const OTHER_VALUED: &[&str] = &[
"separator",
"newline",
"nullvalue",
"vfs",
"maxsize",
"mmap",
"escape",
"pagecache",
"lookaside",
"heap",
];
const NOT_FILES: &[&str] = &["", ":memory:"];
pub(super) fn classify<'a>(
argv: &[String],
program: &str,
stdin: impl Iterator<Item = &'a str>,
ctx: &ShellContext<'_>,
sink: &mut Sink,
) -> Result<Vec<usize>, ClassifyError> {
if !DATABASE_SHELLS.contains(&program) {
return Ok(Vec::new());
}
let read_only = argv[1..].iter().any(|a| option_name(a) == Some("readonly"));
let open = |path: &str, sink: &mut Sink| {
if read_only {
sink.read(ctx, path)
} else {
sink.write(ctx, path)
}
};
let mut database_seen = false;
let mut i = 1;
while let Some(arg) = argv.get(i) {
i += 1;
match option_name(arg) {
Some(name) if SQL_VALUED.contains(&name) || OTHER_VALUED.contains(&name) => {
if let Some(value) = argv.get(i).filter(|_| SQL_VALUED.contains(&name)) {
for path in sql_paths(value) {
open(path, sink)?;
}
}
i += 1;
}
Some("init") => {
if let Some(file) = argv.get(i) {
sink.read(ctx, file)?;
}
i += 1;
}
Some(_) => {}
None if !database_seen => {
database_seen = true;
let file = file_of(arg);
if !NOT_FILES.contains(&file) {
open(file, sink)?;
}
}
None => {
for path in sql_paths(arg) {
open(path, sink)?;
}
}
}
}
for text in stdin {
for path in sql_paths(text) {
open(path, sink)?;
}
}
Ok((1..argv.len()).collect())
}
fn option_name(arg: &str) -> Option<&str> {
let name = arg.strip_prefix('-')?;
Some(name.strip_prefix('-').unwrap_or(name)).filter(|n| !n.is_empty())
}
fn file_of(arg: &str) -> &str {
arg.strip_prefix("file:")
.map_or(arg, |uri| uri.split('?').next().unwrap_or(uri))
}
fn sql_paths(text: &str) -> impl Iterator<Item = &str> {
text.split(|c: char| c.is_whitespace() || "'\";,()=".contains(c))
.map(file_of)
.filter(|word| *word != "/" && paths::looks_like_path(word))
}
#[cfg(test)]
mod tests {
use super::{file_of, option_name, sql_paths};
#[test]
fn options_take_one_or_two_dashes() {
assert_eq!(option_name("-readonly"), Some("readonly"));
assert_eq!(option_name("--readonly"), Some("readonly"));
assert_eq!(option_name("audit.db"), None);
assert_eq!(option_name("-"), None);
}
#[test]
fn uris_name_their_file() {
assert_eq!(file_of("file:~/.moat/audit.db?mode=ro"), "~/.moat/audit.db");
assert_eq!(file_of("audit.db"), "audit.db");
}
#[test]
fn sql_names_the_files_it_opens() {
let sql = "attach '~/.moat/audit.db' as a; select 6 / 2; .output /tmp/out";
assert_eq!(
sql_paths(sql).collect::<Vec<_>>(),
["~/.moat/audit.db", ".output", "/tmp/out"]
);
assert_eq!(sql_paths("select count(*) from events").count(), 0);
}
}