mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-19 09:34:18 +01:00
In the hopes of doing a good job at teaching people what Turso can do, I am adding built-in manual pages. When the CLI starts, it picks a feature at random, and tells the user that the feature exists: ``` Turso v0.2.0-pre.8 Enter ".help" for usage hints. Did you know that Turso supports Change Data Capture? Type .manual cdc to learn more. This software is ALPHA, only use for development, testing, and experimentation. Connected to a transient in-memory database. Use ".open FILENAME" to reopen on a persistent database ``` There is a lot we can do to make this feature world class: - we can automatically compile examples during compile time like rust-doc, to make sure examples used in the manuals always work - we can implement scrolling and navigation - we can document a lot more features But for now, this is a start!
371 lines
11 KiB
Rust
371 lines
11 KiB
Rust
use crate::app::Opts;
|
|
use clap::ValueEnum;
|
|
use std::{
|
|
fmt::{Display, Formatter},
|
|
io::{self, Write},
|
|
sync::Arc,
|
|
};
|
|
use turso_core::{LimboError, StepResult};
|
|
|
|
#[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,
|
|
Line,
|
|
}
|
|
|
|
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 timer: bool,
|
|
pub headers: bool,
|
|
pub mcp: bool,
|
|
pub stats: 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()),
|
|
},
|
|
timer: false,
|
|
headers: false,
|
|
mcp: opts.mcp,
|
|
stats: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
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 struct ApplyWriter<'a> {
|
|
target: &'a Arc<turso_core::Connection>,
|
|
// accumulate raw bytes to support non-utf8 BLOB types
|
|
buf: Vec<u8>,
|
|
}
|
|
|
|
impl<'a> ApplyWriter<'a> {
|
|
pub fn new(target: &'a Arc<turso_core::Connection>) -> Self {
|
|
Self {
|
|
target,
|
|
buf: Vec::new(),
|
|
}
|
|
}
|
|
|
|
// Find the next statement terminator ;\n or ;\r\n in a byte buffer.
|
|
// Returns (end_idx_inclusive, drain_len), where drain_len includes the newline(s).
|
|
fn find_stmt_end(buf: &[u8]) -> Option<(usize, usize)> {
|
|
let mut i = 0;
|
|
while i < buf.len() {
|
|
// Look for ';'
|
|
if buf[i] == b';' {
|
|
// Accept ;\n
|
|
if i + 1 < buf.len() && buf[i + 1] == b'\n' {
|
|
return Some((i, 2));
|
|
}
|
|
// Accept ;\r\n
|
|
if i + 2 < buf.len() && buf[i + 1] == b'\r' && buf[i + 2] == b'\n' {
|
|
return Some((i, 3));
|
|
}
|
|
}
|
|
i += 1;
|
|
}
|
|
None
|
|
}
|
|
|
|
pub fn flush_complete_statements(&mut self) -> io::Result<()> {
|
|
while let Some((end_inclusive, drain_len)) = Self::find_stmt_end(&self.buf) {
|
|
// Copy stmt bytes [0..=end_inclusive]
|
|
let stmt_bytes = self.buf[..=end_inclusive].to_vec();
|
|
// Drain including the trailing newline(s)
|
|
self.buf.drain(..end_inclusive + drain_len);
|
|
self.exec_stmt_bytes(&stmt_bytes)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// Handle final trailing statement that ends with ';' followed only by ASCII whitespace.
|
|
pub fn finish(mut self) -> io::Result<()> {
|
|
// Skip if buffer empty or no ';'
|
|
if let Some(semicolon_pos) = self.buf.iter().rposition(|&b| b == b';') {
|
|
// Are all bytes after ';' ASCII whitespace?
|
|
if self.buf[semicolon_pos + 1..]
|
|
.iter()
|
|
.all(|&b| matches!(b, b' ' | b'\t' | b'\r' | b'\n'))
|
|
{
|
|
let stmt_bytes = self.buf[..=semicolon_pos].to_vec();
|
|
self.buf.clear();
|
|
self.exec_stmt_bytes(&stmt_bytes)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn exec_stmt_bytes(&self, stmt_bytes: &[u8]) -> io::Result<()> {
|
|
// SQL must be UTF-8. If not, surface a clear error.
|
|
let sql = std::str::from_utf8(stmt_bytes).map_err(|e| {
|
|
io::Error::new(io::ErrorKind::InvalidData, format!("non-UTF8 SQL: {e}"))
|
|
})?;
|
|
self.exec_stmt(sql)
|
|
.map_err(|e| io::Error::other(e.to_string()))
|
|
}
|
|
|
|
fn exec_stmt(&self, sql: &str) -> Result<(), LimboError> {
|
|
match self.target.query(sql) {
|
|
Ok(Some(mut rows)) => loop {
|
|
match rows.step()? {
|
|
StepResult::Row => {}
|
|
StepResult::IO => rows.run_once()?,
|
|
StepResult::Done | StepResult::Interrupt => break,
|
|
StepResult::Busy => {
|
|
return Err(LimboError::InternalError("target database is busy".into()))
|
|
}
|
|
}
|
|
},
|
|
Ok(None) => {}
|
|
Err(e) => return Err(e),
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl<'a> Write for ApplyWriter<'a> {
|
|
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
|
|
self.buf.extend_from_slice(data);
|
|
self.flush_complete_statements()?;
|
|
Ok(data.len())
|
|
}
|
|
fn flush(&mut self) -> io::Result<()> {
|
|
self.flush_complete_statements()
|
|
}
|
|
}
|
|
|
|
pub trait ProgressSink {
|
|
fn on<S: Display>(&mut self, _p: S) {}
|
|
}
|
|
|
|
pub struct NoopProgress;
|
|
impl ProgressSink for NoopProgress {}
|
|
|
|
pub struct StderrProgress;
|
|
|
|
impl ProgressSink for StderrProgress {
|
|
fn on<S: Display>(&mut self, s: S) {
|
|
eprintln!("{s}... done");
|
|
}
|
|
}
|
|
|
|
pub const BEFORE_HELP_MSG: &str = r#"
|
|
|
|
Turso SQL Shell Help
|
|
==============
|
|
Welcome to the Turso 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 Turso 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 databases:
|
|
.databases
|
|
|
|
6. To list all available SQL opcodes:
|
|
.opcodes
|
|
|
|
7. To change the current output mode to 'pretty':
|
|
.mode pretty
|
|
|
|
8. Send output to STDOUT if no file is specified:
|
|
.output
|
|
|
|
9. To change the current working directory to '/tmp':
|
|
.cd /tmp
|
|
|
|
10. Show the current values of settings:
|
|
.show
|
|
|
|
11. To import csv file 'sample.csv' into 'csv_table' table:
|
|
.import --csv sample.csv csv_table
|
|
|
|
12. To display the database contents as SQL:
|
|
.dump
|
|
|
|
13. To load an extension library:
|
|
.load /target/debug/liblimbo_regexp
|
|
|
|
14. To list all available VFS:
|
|
.listvfs
|
|
|
|
15. To show names of indexes:
|
|
.indexes ?TABLE?
|
|
|
|
16. To turn on column headers in list mode:
|
|
.headers on
|
|
|
|
17. To turn off column headers in list mode:
|
|
.headers off
|
|
|
|
18. To clone the open database to another file:
|
|
.clone output_file.db
|
|
|
|
19. To view manual pages for features:
|
|
.manual mcp # View MCP server documentation
|
|
.man # List all available manuals
|
|
|
|
Note:
|
|
- All SQL commands must end with a semicolon (;).
|
|
- Special commands start with a dot (.) and are not required to end with a semicolon."#;
|