mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-27 04:54:21 +01:00
The SQLite command line has facilities to ingest things like csv, and other formats. But here we are, in 2025, and I asked Claude if Turso's CLI should, in the same vein, have a native MCP server. Claude told me: "You're absolutely right!" "That's a great insight!" "That's a fantastic idea!" and then proceeded to help me with the boilerplate for this beautiful server. Rust has a crate, mcp_server, that implements an mcp_server trait. However, that depends on Tokio, and I think that would bloat our binary too much. I have also considered implementing an MCP server that operates on a directory and allows to list many SQLite files, but figured that would be a good job for a more advanced and specialized server, not for the one that comes by default with the CLI. Let's go for simple.
241 lines
6.8 KiB
Rust
241 lines
6.8 KiB
Rust
use crate::app::Opts;
|
|
use clap::ValueEnum;
|
|
use std::{
|
|
fmt::{Display, Formatter},
|
|
io::{self, Write},
|
|
sync::Arc,
|
|
};
|
|
|
|
#[derive(Copy, Clone)]
|
|
pub enum DbLocation {
|
|
Memory,
|
|
Path,
|
|
}
|
|
|
|
#[allow(clippy::enum_variant_names)]
|
|
#[derive(Clone, Debug)]
|
|
pub enum Io {
|
|
Syscall,
|
|
#[cfg(all(target_os = "linux", feature = "io_uring"))]
|
|
IoUring,
|
|
External(String),
|
|
Memory,
|
|
}
|
|
|
|
impl Display for Io {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Io::Memory => write!(f, "memory"),
|
|
Io::Syscall => write!(f, "syscall"),
|
|
#[cfg(all(target_os = "linux", feature = "io_uring"))]
|
|
Io::IoUring => write!(f, "io_uring"),
|
|
Io::External(str) => write!(f, "{str}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for Io {
|
|
/// Custom Default impl with cfg! macro, to provide compile-time default to Clap based on platform
|
|
/// The cfg! could be elided, but Clippy complains
|
|
/// The default value can still be overridden with the Clap argument
|
|
fn default() -> Self {
|
|
match cfg!(all(target_os = "linux", feature = "io_uring")) {
|
|
true => {
|
|
#[cfg(all(target_os = "linux", feature = "io_uring"))]
|
|
{
|
|
Io::Syscall // FIXME: make io_uring faster so it can be the default
|
|
}
|
|
#[cfg(any(
|
|
not(target_os = "linux"),
|
|
all(target_os = "linux", not(feature = "io_uring"))
|
|
))]
|
|
{
|
|
Io::Syscall
|
|
}
|
|
}
|
|
false => Io::Syscall,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]
|
|
pub enum OutputMode {
|
|
List,
|
|
Pretty,
|
|
}
|
|
|
|
impl std::fmt::Display for OutputMode {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
self.to_possible_value()
|
|
.expect("no values are skipped")
|
|
.get_name()
|
|
.fmt(f)
|
|
}
|
|
}
|
|
|
|
pub struct Settings {
|
|
pub output_filename: String,
|
|
pub db_file: String,
|
|
pub null_value: String,
|
|
pub output_mode: OutputMode,
|
|
pub echo: bool,
|
|
pub is_stdout: bool,
|
|
pub io: Io,
|
|
pub tracing_output: Option<String>,
|
|
pub timer: bool,
|
|
pub headers: bool,
|
|
pub mcp: bool,
|
|
}
|
|
|
|
impl From<Opts> for Settings {
|
|
fn from(opts: Opts) -> Self {
|
|
Self {
|
|
null_value: String::new(),
|
|
output_mode: opts.output_mode,
|
|
echo: false,
|
|
is_stdout: opts.output.is_empty(),
|
|
output_filename: opts.output,
|
|
db_file: opts
|
|
.database
|
|
.as_ref()
|
|
.map_or(":memory:".to_string(), |p| p.to_string_lossy().to_string()),
|
|
io: match opts.vfs.as_ref().unwrap_or(&String::new()).as_str() {
|
|
"memory" | ":memory:" => Io::Memory,
|
|
"syscall" => Io::Syscall,
|
|
#[cfg(all(target_os = "linux", feature = "io_uring"))]
|
|
"io_uring" => Io::IoUring,
|
|
"" => Io::default(),
|
|
vfs => Io::External(vfs.to_string()),
|
|
},
|
|
tracing_output: opts.tracing_output,
|
|
timer: false,
|
|
headers: false,
|
|
mcp: opts.mcp,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for Settings {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(
|
|
f,
|
|
"Settings:\nOutput mode: {}\nDB: {}\nOutput: {}\nNull value: {}\nCWD: {}\nEcho: {}\nHeaders: {}",
|
|
self.output_mode,
|
|
self.db_file,
|
|
match self.is_stdout {
|
|
true => "STDOUT",
|
|
false => &self.output_filename,
|
|
},
|
|
self.null_value,
|
|
std::env::current_dir().unwrap().display(),
|
|
match self.echo {
|
|
true => "on",
|
|
false => "off",
|
|
},
|
|
match self.headers {
|
|
true => "on",
|
|
false => "off",
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
pub fn get_writer(output: &str) -> Box<dyn Write> {
|
|
match output {
|
|
"" => Box::new(io::stdout()),
|
|
_ => match std::fs::File::create(output) {
|
|
Ok(file) => Box::new(file),
|
|
Err(e) => {
|
|
eprintln!("Error: {e}");
|
|
Box::new(io::stdout())
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn get_io(db_location: DbLocation, io_choice: &str) -> anyhow::Result<Arc<dyn turso_core::IO>> {
|
|
Ok(match db_location {
|
|
DbLocation::Memory => Arc::new(turso_core::MemoryIO::new()),
|
|
DbLocation::Path => {
|
|
match io_choice {
|
|
"memory" => Arc::new(turso_core::MemoryIO::new()),
|
|
"syscall" => {
|
|
// We are building for Linux/macOS and syscall backend has been selected
|
|
#[cfg(target_family = "unix")]
|
|
{
|
|
Arc::new(turso_core::UnixIO::new()?)
|
|
}
|
|
// We are not building for Linux/macOS and syscall backend has been selected
|
|
#[cfg(not(target_family = "unix"))]
|
|
{
|
|
Arc::new(turso_core::PlatformIO::new()?)
|
|
}
|
|
}
|
|
// We are building for Linux and io_uring backend has been selected
|
|
#[cfg(all(target_os = "linux", feature = "io_uring"))]
|
|
"io_uring" => Arc::new(turso_core::UringIO::new()?),
|
|
_ => Arc::new(turso_core::PlatformIO::new()?),
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
pub const BEFORE_HELP_MSG: &str = r#"
|
|
|
|
Limbo SQL Shell Help
|
|
==============
|
|
Welcome to the Limbo SQL Shell! You can execute any standard SQL command here.
|
|
In addition to standard SQL commands, the following special commands are available:"#;
|
|
pub const AFTER_HELP_MSG: &str = r#"Usage Examples:
|
|
---------------
|
|
1. To quit the Limbo SQL Shell:
|
|
.quit
|
|
|
|
2. To open a database file at path './employees.db':
|
|
.open employees.db
|
|
|
|
3. To view the schema of a table named 'employees':
|
|
.schema employees
|
|
|
|
4. To list all tables:
|
|
.tables
|
|
|
|
5. To list all available SQL opcodes:
|
|
.opcodes
|
|
|
|
6. To change the current output mode to 'pretty':
|
|
.mode pretty
|
|
|
|
7. Send output to STDOUT if no file is specified:
|
|
.output
|
|
|
|
8. To change the current working directory to '/tmp':
|
|
.cd /tmp
|
|
|
|
9. Show the current values of settings:
|
|
.show
|
|
|
|
10. To import csv file 'sample.csv' into 'csv_table' table:
|
|
.import --csv sample.csv csv_table
|
|
|
|
11. To display the database contents as SQL:
|
|
.dump
|
|
|
|
12. To load an extension library:
|
|
.load /target/debug/liblimbo_regexp
|
|
|
|
13. To list all available VFS:
|
|
.listvfs
|
|
14. To show names of indexes:
|
|
.indexes ?TABLE?
|
|
|
|
15. To turn on column headers in list mode:
|
|
.headers on
|
|
|
|
16. To turn off column headers in list mode:
|
|
.headers off
|
|
|
|
Note:
|
|
- All SQL commands must end with a semicolon (;).
|
|
- Special commands start with a dot (.) and are not required to end with a semicolon."#;
|