新增: MySQL/SSH 代理工具

- mysql-proxy: MySQL HTTP 代理,连接池复用
- ssh-proxy: SSH HTTP 代理,会话复用
- mysql-cli: 轻量级 MySQL CLI 工具

功能特性:
- 延迟初始化,启动快
- CLI 和 HTTP API 双模式
- 请求日志支持
- 错误友好提示
- JSON 极简输出格式
This commit is contained in:
2026-03-19 14:03:12 +08:00
commit 11203f036f
24 changed files with 3794 additions and 0 deletions

34
mysql-cli/Cargo.toml Normal file
View File

@@ -0,0 +1,34 @@
[package]
name = "mysql-cli"
version = "0.1.0"
edition = "2021"
description = "A lightweight MySQL command-line tool written in Rust"
[dependencies]
# MySQL driver
mysql = "25"
# Command line argument parsing
clap = { version = "4", features = ["derive"] }
# REPL with history and completion
rustyline = { version = "14", features = ["derive"] }
# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# CSV output
csv = "1"
# Error handling
anyhow = "1"
thiserror = "1"
# Check if stdin is a tty
atty = "0.2"
[profile.release]
opt-level = "z"
lto = true
strip = true

148
mysql-cli/README.md Normal file
View File

@@ -0,0 +1,148 @@
# MySQL CLI (Rust)
> 💡 **快速开始**:运行 `mysql --help` 查看完整使用文档
一个轻量级的 Rust 语言 MySQL 命令行工具,兼容 MySQL 官方客户端用法,可以在未安装 MySQL 客户端的系统中执行 SQL 语句。
## 功能特性
-**交互式模式**:支持完整的 REPL 交互式 Shell
-**批量执行**:支持从 SQL 文件执行多条语句
-**多种输出格式**支持表格、CSV、JSON、垂直格式
-**完整 SQL 支持**:支持 SELECT、INSERT、UPDATE、DELETE 等所有 SQL 语句
-**特殊命令**:支持 USE、SHOW、DESCRIBE、EXPLAIN 等 MySQL 命令
-**紧凑体积**:编译后仅 2.5MB,无需安装 MySQL 客户端
## 编译
```bash
cargo build --release
```
## 使用方法
### 基本语法
```bash
./mysql.exe -h <host> -P <port> -u <username> -p <password> -D <database> -e "<SQL>"
```
### 参数说明
| 参数 | 说明 | 默认值 |
|------|------|--------|
| -h | MySQL 主机地址 | 127.0.0.1 |
| -P | MySQL 端口 | 3306 |
| -u | 用户名 | root |
| -p | 密码 | (空) |
| -D | 数据库名 | (空) |
| -e | 要执行的 SQL 语句 | (可选) |
| -f | 从 SQL 文件执行 | (可选) |
| -F | 输出格式table, csv, json, vertical | table |
| -o | 输出到文件 | stdout |
| -i | 强制进入交互模式 | (可选) |
**注意**:不指定 `-e``-f` 时,需要使用 `-i` 参数进入交互模式
### 使用示例
#### 1. 交互式模式
进入交互式 Shell
```bash
# 基本连接并进入交互模式
mysql -p123456 -i
# 指定主机和用户
mysql -h127.0.0.1 -uroot -p123456 -i
```
交互式命令:
- `use <database>` - 切换数据库
- `status` - 显示连接状态
- `source <file>` - 执行 SQL 文件
- `format <type>` - 设置输出格式table, csv, json, vertical
- `output <file>` - 设置输出文件
- `exit / quit` - 退出程序
#### 2. 直接执行 SQL
```bash
# 查询数据
mysql -h127.0.0.1 -uroot -p123456 -Dmydb -e "SELECT * FROM users LIMIT 10"
# 显示表结构
mysql -p123456 -Dmydb -e "DESCRIBE users"
# 显示所有数据库
mysql -p123456 -e "SHOW DATABASES"
# 插入数据
mysql -p123456 -Dmydb -e "INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')"
# 更新数据
mysql -p123456 -Dmydb -e "UPDATE users SET email='new@email.com' WHERE id=1"
# 删除数据
mysql -p123456 -Dmydb -e "DELETE FROM users WHERE id=1"
```
#### 3. 从 SQL 文件执行
```bash
# 执行 SQL 文件
mysql -p123456 -f script.sql
# 执行并输出到 CSV
mysql -p123456 -f script.sql -F csv -o result.csv
```
#### 4. 不同输出格式
```bash
# CSV 格式
mysql -p123456 -e "SELECT * FROM users" -F csv
# JSON 格式
mysql -p123456 -e "SELECT * FROM users" -F json
# 垂直格式(适合字段较多的记录)
mysql -p123456 -e "SELECT * FROM users" -F vertical
```
## 项目结构
```
mysql-cli/
├── Cargo.toml # Rust 项目配置
├── README.md # 项目文档
└── src/
├── main.rs # 主程序入口
├── config.rs # 配置管理
├── db.rs # 数据库连接
├── executor.rs # SQL 执行引擎
└── repl.rs # 交互式 REPL
```
## 依赖
- Rust 1.70+
- mysql (25.x)
- clap (4.x) - 命令行参数解析
- rustyline (14.x) - 交互式 REPL
- serde / serde_json - JSON 序列化
- csv - CSV 输出
## 与 Go 版本对比
| 功能 | Rust 版本 | Go 版本 |
|------|-----------|---------|
| 交互式模式 | ✅ | ✅ |
| SQL 文件执行 | ✅ | ✅ |
| 输出格式 | 表格/CSV/JSON/垂直 | 表格/CSV/JSON/垂直 |
| 二进制大小 | ~2.5MB | ~7.3MB |
| 内存占用 | 更低 | 较高 |
## License
MIT

115
mysql-cli/src/config.rs Normal file
View File

@@ -0,0 +1,115 @@
use std::fmt;
/// 数据库连接配置
#[derive(Debug, Clone)]
pub struct Config {
pub host: String,
pub port: u16,
pub username: String,
pub password: String,
pub database: String,
}
impl Default for Config {
fn default() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 3306,
username: "root".to_string(),
password: String::new(),
database: String::new(),
}
}
}
impl Config {
/// 构建 MySQL DSN
pub fn build_dsn(&self) -> String {
format!(
"mysql://{}:{}@{}:{}/{}",
self.username,
urlencoding(&self.password),
self.host,
self.port,
self.database
)
}
/// 构建不带数据库的 DSN
pub fn build_dsn_without_db(&self) -> String {
format!(
"mysql://{}:{}@{}:{}/",
self.username,
urlencoding(&self.password),
self.host,
self.port
)
}
}
impl fmt::Display for Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}@{}:{}",
self.username, self.host, self.port
)
}
}
/// URL 编码密码中的特殊字符
fn urlencoding(s: &str) -> String {
let mut result = String::new();
for c in s.chars() {
match c {
'@' => result.push_str("%40"),
':' => result.push_str("%3A"),
'/' => result.push_str("%2F"),
'?' => result.push_str("%3F"),
'#' => result.push_str("%23"),
'[' => result.push_str("%5B"),
']' => result.push_str("%5D"),
'!' => result.push_str("%21"),
'$' => result.push_str("%24"),
'&' => result.push_str("%26"),
'\'' => result.push_str("%27"),
'(' => result.push_str("%28"),
')' => result.push_str("%29"),
'*' => result.push_str("%2A"),
'+' => result.push_str("%2B"),
',' => result.push_str("%2C"),
';' => result.push_str("%3B"),
'=' => result.push_str("%3D"),
'%' => result.push_str("%25"),
' ' => result.push_str("%20"),
_ => result.push(c),
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_dsn() {
let config = Config {
host: "localhost".to_string(),
port: 3306,
username: "root".to_string(),
password: "123456".to_string(),
database: "test".to_string(),
};
assert_eq!(
config.build_dsn(),
"mysql://root:123456@localhost:3306/test"
);
}
#[test]
fn test_urlencoding() {
assert_eq!(urlencoding("a@b:c"), "a%40b%3Ac");
assert_eq!(urlencoding("p@ss!word"), "p%40ss%21word");
}
}

60
mysql-cli/src/db.rs Normal file
View File

@@ -0,0 +1,60 @@
use anyhow::Result;
use mysql::{Pool, PooledConn, Opts, prelude::*};
use crate::config::Config;
/// 连接数据库
pub fn connect(cfg: &Config) -> Result<Pool> {
let dsn = if cfg.database.is_empty() {
cfg.build_dsn_without_db()
} else {
cfg.build_dsn()
};
let opts: Opts = Opts::from_url(&dsn)?;
let pool = Pool::new(opts)?;
// 测试连接
let mut conn = pool.get_conn()?;
conn.query::<String, _>("SELECT 1")?;
Ok(pool)
}
/// 获取连接
pub fn get_conn(pool: &Pool) -> Result<PooledConn> {
Ok(pool.get_conn()?)
}
/// 获取服务器版本
pub fn get_server_version(conn: &mut PooledConn) -> String {
conn.query_first::<String, _>("SELECT VERSION()")
.ok()
.flatten()
.unwrap_or_else(|| "Unknown".to_string())
}
/// 获取当前数据库
pub fn get_current_database(conn: &mut PooledConn) -> Option<String> {
conn.query_first::<String, _>("SELECT DATABASE()").ok().flatten()
}
/// 获取所有数据库
pub fn get_databases(conn: &mut PooledConn) -> Vec<String> {
conn.query::<String, _>("SHOW DATABASES")
.unwrap_or_default()
}
/// 获取所有表名
pub fn get_tables(conn: &mut PooledConn) -> Vec<String> {
conn.query::<String, _>("SHOW TABLES")
.unwrap_or_default()
}
/// 获取表的列信息
pub fn get_columns(conn: &mut PooledConn, table: &str) -> Vec<String> {
let sql = format!("SHOW COLUMNS FROM `{}`", table);
conn.query_map(sql, |row: mysql::Row| {
row.get::<String, _>(0).unwrap_or_default()
}).unwrap_or_default()
}

348
mysql-cli/src/executor.rs Normal file
View File

@@ -0,0 +1,348 @@
use anyhow::{Result, bail};
use mysql::{Pool, PooledConn, prelude::*, Row, Value};
use std::fs::File;
use std::io::{self, Write, BufWriter};
/// 输出格式
#[derive(Debug, Clone, Copy, Default)]
pub enum OutputFormat {
#[default]
Table,
Csv,
Json,
Vertical,
}
impl std::str::FromStr for OutputFormat {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"table" => Ok(Self::Table),
"csv" => Ok(Self::Csv),
"json" => Ok(Self::Json),
"vertical" => Ok(Self::Vertical),
_ => bail!("Invalid format: {}. Valid formats: table, csv, json, vertical", s),
}
}
}
/// SQL 执行器
pub struct Executor {
pool: Pool,
format: OutputFormat,
output: Option<String>,
}
impl Executor {
pub fn new(pool: Pool, format: OutputFormat, output: Option<String>) -> Self {
Self { pool, format, output }
}
/// 执行 SQL支持多条语句
pub fn execute(&self, query: &str) -> Result<()> {
let queries = split_queries(query);
let mut conn = self.pool.get_conn()?;
for (i, q) in queries.iter().enumerate() {
if q.trim().is_empty() {
continue;
}
if i > 0 {
println!();
}
self.execute_one(&mut conn, q)?;
}
Ok(())
}
/// 执行单条 SQL
fn execute_one(&self, conn: &mut PooledConn, query: &str) -> Result<()> {
let query = query.trim();
if query.is_empty() {
return Ok(());
}
let upper_query = query.to_uppercase();
let is_query = upper_query.starts_with("SELECT")
|| upper_query.starts_with("SHOW")
|| upper_query.starts_with("DESCRIBE")
|| upper_query.starts_with("DESC ")
|| upper_query.starts_with("EXPLAIN")
|| upper_query.starts_with("WITH");
if is_query {
self.execute_query(conn, query)
} else {
self.execute_exec(conn, query)
}
}
/// 执行查询语句
fn execute_query(&self, conn: &mut PooledConn, query: &str) -> Result<()> {
let result = conn.query_iter(query)?;
let mut column_names: Vec<String> = Vec::new();
let mut data: Vec<Vec<Option<String>>> = Vec::new();
// 获取数据
for row_result in result {
let row = row_result?;
// 从第一行获取列名
if column_names.is_empty() {
column_names = row.columns()
.iter()
.map(|c| c.name_str().to_string())
.collect();
}
let values = row_to_strings(&row);
data.push(values);
}
// 输出结果
self.output_result(&column_names, &data)?;
// 显示行数
if !data.is_empty() {
println!();
println!("{} rows in set", data.len());
}
Ok(())
}
/// 执行非查询语句
fn execute_exec(&self, conn: &mut PooledConn, query: &str) -> Result<()> {
let result = conn.query_iter(query)?;
let upper_query = query.to_uppercase();
if upper_query.starts_with("INSERT")
|| upper_query.starts_with("UPDATE")
|| upper_query.starts_with("DELETE")
|| upper_query.starts_with("REPLACE")
{
let affected = result.affected_rows();
if upper_query.starts_with("INSERT") {
let last_id = result.last_insert_id().unwrap_or(0);
println!("Query OK, {} row affected, last insert ID: {}", affected, last_id);
} else {
println!("Query OK, {} row affected", affected);
}
} else {
println!("Query OK");
}
Ok(())
}
/// 根据格式输出结果
fn output_result(&self, columns: &[String], data: &[Vec<Option<String>>]) -> Result<()> {
let mut output: Box<dyn Write> = match &self.output {
Some(path) => Box::new(BufWriter::new(File::create(path)?)),
None => Box::new(BufWriter::new(io::stdout())),
};
match self.format {
OutputFormat::Table => self.output_table(&mut output, columns, data),
OutputFormat::Csv => self.output_csv(&mut output, columns, data),
OutputFormat::Json => self.output_json(&mut output, columns, data),
OutputFormat::Vertical => self.output_vertical(&mut output, columns, data),
}
}
/// 表格格式输出
fn output_table(&self, output: &mut dyn Write, columns: &[String], data: &[Vec<Option<String>>]) -> Result<()> {
// 计算列宽
let mut widths: Vec<usize> = columns.iter().map(|c| c.len()).collect();
for row in data {
for (i, val) in row.iter().enumerate() {
let len = val.as_ref().map_or(4, |v| v.len());
if len > widths[i] {
widths[i] = len;
}
}
}
// 打印表头
print_table_row(output, columns, &widths)?;
print_table_separator(output, &widths)?;
// 打印数据行
for row in data {
let str_row: Vec<String> = row.iter().map(|v| v.clone().unwrap_or_else(|| "NULL".to_string())).collect();
print_table_row(output, &str_row, &widths)?;
}
Ok(())
}
/// CSV 格式输出
fn output_csv(&self, output: &mut dyn Write, columns: &[String], data: &[Vec<Option<String>>]) -> Result<()> {
let mut writer = csv::Writer::from_writer(output);
writer.write_record(columns)?;
for row in data {
let str_row: Vec<String> = row.iter().map(|v| v.clone().unwrap_or_default()).collect();
writer.write_record(&str_row)?;
}
writer.flush()?;
Ok(())
}
/// JSON 格式输出
fn output_json(&self, output: &mut dyn Write, columns: &[String], data: &[Vec<Option<String>>]) -> Result<()> {
let result: Vec<serde_json::Map<String, serde_json::Value>> = data
.iter()
.map(|row| {
columns
.iter()
.zip(row.iter())
.map(|(col, val)| {
let v = match val {
Some(s) => serde_json::Value::String(s.clone()),
None => serde_json::Value::Null,
};
(col.clone(), v)
})
.collect()
})
.collect();
let json = serde_json::to_string_pretty(&result)?;
writeln!(output, "{}", json)?;
Ok(())
}
/// 垂直格式输出
fn output_vertical(&self, output: &mut dyn Write, columns: &[String], data: &[Vec<Option<String>>]) -> Result<()> {
let max_len = columns.iter().map(|c| c.len()).max().unwrap_or(0);
for (row_idx, row) in data.iter().enumerate() {
if row_idx > 0 {
writeln!(output)?;
}
for (i, val) in row.iter().enumerate() {
let v = val.as_ref().map(|s| s.as_str()).unwrap_or("NULL");
writeln!(output, "{:width$}: {}", columns[i], v, width = max_len)?;
}
}
Ok(())
}
/// 从文件执行 SQL
pub fn execute_from_file(&self, filename: &str) -> Result<()> {
let content = std::fs::read_to_string(filename)?;
self.execute(&content)
}
}
/// 将 Row 转换为字符串向量
fn row_to_strings(row: &Row) -> Vec<Option<String>> {
row.columns()
.iter()
.enumerate()
.map(|(i, _col)| {
match row.get::<Value, usize>(i) {
Some(value) => value_to_string(&value),
None => None,
}
})
.collect()
}
/// 将 Value 转换为字符串
fn value_to_string(value: &Value) -> Option<String> {
match value {
Value::NULL => None,
Value::Bytes(bytes) => String::from_utf8(bytes.clone()).ok(),
Value::Int(i) => Some(i.to_string()),
Value::UInt(u) => Some(u.to_string()),
Value::Float(f) => Some(f.to_string()),
Value::Double(d) => Some(d.to_string()),
Value::Date(year, month, day, hour, min, sec, micro) => {
match (hour, min, sec, micro) {
(0, 0, 0, 0) => Some(format!("{:04}-{:02}-{:02}", year, month, day)),
_ => Some(format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}", year, month, day, hour, min, sec)),
}
}
Value::Time(neg, days, hours, minutes, seconds, microseconds) => {
let sign = if *neg { "-" } else { "" };
Some(format!("{}{} {}:{:02}:{:02}.{:06}", sign, days, hours, minutes, seconds, microseconds))
}
}
}
/// 分割多个 SQL 语句
fn split_queries(query: &str) -> Vec<String> {
let mut queries = Vec::new();
let mut current = String::new();
let mut in_quotes = false;
let mut quote_char = ' ';
let chars: Vec<char> = query.chars().collect();
for i in 0..chars.len() {
let c = chars[i];
match c {
'\'' | '"' | '`' if !in_quotes => {
in_quotes = true;
quote_char = c;
current.push(c);
}
'\'' | '"' | '`' if in_quotes && c == quote_char => {
// 检查是否是转义
let is_escaped = i > 0 && chars[i - 1] == '\\';
if !is_escaped {
in_quotes = false;
}
current.push(c);
}
';' if !in_quotes => {
queries.push(current.trim().to_string());
current.clear();
}
_ => {
current.push(c);
}
}
}
if !current.trim().is_empty() {
queries.push(current.trim().to_string());
}
queries
}
/// 打印表格行
fn print_table_row(output: &mut dyn Write, data: &[String], widths: &[usize]) -> Result<()> {
for (i, val) in data.iter().enumerate() {
if i > 0 {
write!(output, " | ")?;
}
write!(output, "{:<width$}", val, width = widths[i])?;
}
writeln!(output)?;
Ok(())
}
/// 打印表格分隔线
fn print_table_separator(output: &mut dyn Write, widths: &[usize]) -> Result<()> {
for (i, w) in widths.iter().enumerate() {
if i > 0 {
write!(output, "-+-")?;
} else {
write!(output, "+")?;
}
for _ in 0..*w {
write!(output, "-")?;
}
}
writeln!(output, "+")?;
Ok(())
}

132
mysql-cli/src/main.rs Normal file
View File

@@ -0,0 +1,132 @@
use anyhow::Result;
use clap::Parser;
use std::io::{self, Read};
mod config;
mod db;
mod executor;
mod repl;
use config::Config;
use executor::{Executor, OutputFormat};
/// MySQL CLI - A lightweight MySQL command-line tool
#[derive(Parser, Debug)]
#[command(name = "mysql")]
#[command(about = "A lightweight MySQL command-line tool", long_about = None)]
struct Args {
/// MySQL host
#[arg(short = 'h', long, default_value = "127.0.0.1")]
host: String,
/// MySQL port
#[arg(short = 'P', long, default_value_t = 3306)]
port: u16,
/// MySQL username
#[arg(short = 'u', long, default_value = "root")]
username: String,
/// MySQL password
#[arg(short = 'p', long, default_value = "")]
password: String,
/// Database name
#[arg(short = 'D', long, default_value = "")]
database: String,
/// Execute SQL statement
#[arg(short = 'e', long)]
execute: Option<String>,
/// Execute SQL from file
#[arg(short = 'f', long)]
file: Option<String>,
/// Output format: table, csv, json, vertical
#[arg(short = 'F', long, default_value = "table")]
format: String,
/// Output file (default: stdout)
#[arg(short = 'o', long)]
output: Option<String>,
/// Interactive mode (REPL)
#[arg(short = 'i', long)]
interactive: bool,
}
fn main() -> Result<()> {
let args = Args::parse();
let config = Config {
host: args.host,
port: args.port,
username: args.username,
password: args.password,
database: args.database,
};
// 连接数据库
let pool = match db::connect(&config) {
Ok(p) => p,
Err(e) => {
eprintln!("Error connecting to database: {}", e);
std::process::exit(1);
}
};
let format: OutputFormat = args.format.parse().unwrap_or_default();
let executor = Executor::new(pool.clone(), format, args.output.clone());
// 检测 stdin 是否被重定向
let is_piped = !atty::is(atty::Stream::Stdin);
// 确定运行模式
if let Some(file) = &args.file {
// 从文件执行
if let Err(e) = executor.execute_from_file(file) {
eprintln!("Error executing file: {}", e);
std::process::exit(1);
}
} else if let Some(query) = &args.execute {
// 执行 SQL
if let Err(e) = executor.execute(query) {
eprintln!("Error executing SQL: {}", e);
std::process::exit(1);
}
} else if is_piped {
// stdin 被重定向,读取所有输入并执行
let mut content = String::new();
if let Err(e) = io::stdin().read_to_string(&mut content) {
eprintln!("Error reading stdin: {}", e);
std::process::exit(1);
}
if !content.trim().is_empty() {
if let Err(e) = executor.execute(&content) {
eprintln!("Error executing SQL: {}", e);
std::process::exit(1);
}
}
} else if args.interactive {
// 交互模式
if let Err(e) = repl::run_repl(pool, config) {
eprintln!("Error: {}", e);
std::process::exit(1);
}
} else {
// 没有指定任何操作,显示使用提示
eprintln!("ERROR: Missing required argument.");
eprintln!();
eprintln!("Usage:");
eprintln!(" mysql -h <host> -P <port> -u <user> -p <password> -D <database> -e \"<SQL>\"");
eprintln!(" mysql -i # Interactive mode");
eprintln!(" mysql -f <file> # Execute SQL file");
eprintln!(" echo \"SELECT 1\" | mysql # Execute from stdin");
eprintln!();
eprintln!("Run 'mysql --help' for detailed documentation.");
std::process::exit(1);
}
Ok(())
}

297
mysql-cli/src/repl.rs Normal file
View File

@@ -0,0 +1,297 @@
use anyhow::Result;
use mysql::{Pool, prelude::*};
use rustyline::error::ReadlineError;
use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
use rustyline::history::DefaultHistory;
use rustyline::{Completer, CompletionType, Config, Context, EditMode, Editor, Helper, Validator};
use std::borrow::Cow;
use std::time::Instant;
use crate::config::Config as DbConfig;
use crate::db;
use crate::executor::{Executor, OutputFormat};
/// REPL 状态
pub struct ReplState {
config: DbConfig,
format: OutputFormat,
output: Option<String>,
}
impl ReplState {
pub fn new(config: DbConfig) -> Self {
Self {
config,
format: OutputFormat::Table,
output: None,
}
}
fn prompt(&self) -> String {
let db = if self.config.database.is_empty() {
"(none)"
} else {
&self.config.database
};
format!("mysql [{}]> ", db)
}
}
/// 智能补全器
#[derive(Completer, Helper, Validator)]
pub struct SqlCompleter {
sql_keywords: Vec<&'static str>,
}
impl SqlCompleter {
pub fn new() -> Self {
Self {
sql_keywords: vec![
"SELECT", "INSERT", "UPDATE", "DELETE", "DROP",
"CREATE", "ALTER", "SHOW", "DESCRIBE", "DESC", "USE",
"FROM", "WHERE", "ORDER", "GROUP", "LIMIT", "OFFSET",
"JOIN", "LEFT", "RIGHT", "INNER", "ON", "AS",
"AND", "OR", "NOT", "IN", "LIKE", "BETWEEN",
"BY", "TABLE", "DATABASE", "INDEX", "VIEW",
"SET", "VALUES", "INTO", "DISTINCT", "COUNT",
"SUM", "AVG", "MAX", "MIN", "NULL", "IS",
],
}
}
}
impl Hinter for SqlCompleter {
type Hint = String;
fn hint(&self, line: &str, pos: usize, _ctx: &Context<'_>) -> Option<String> {
if line.is_empty() || pos < line.len() {
return None;
}
let line_upper = line.to_uppercase();
for keyword in &self.sql_keywords {
if keyword.starts_with(&line_upper) && keyword.len() > line.len() {
return Some(keyword[line.len()..].to_string());
}
}
None
}
}
impl Highlighter for SqlCompleter {
fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
Cow::Borrowed(line)
}
fn highlight_char(&self, _line: &str, _pos: usize, _forced: bool) -> bool {
false
}
}
/// 运行交互式 REPL
pub fn run_repl(pool: Pool, config: DbConfig) -> Result<()> {
let mut state = ReplState::new(config);
let executor = Executor::new(pool.clone(), state.format, state.output.clone());
// 打印欢迎信息
print_welcome(&pool, &state.config)?;
// 初始化 rustyline
let rl_config = Config::builder()
.history_ignore_space(true)
.completion_type(CompletionType::List)
.edit_mode(EditMode::Emacs)
.build();
let mut rl: Editor<SqlCompleter, DefaultHistory> = Editor::with_config(rl_config)?;
rl.set_helper(Some(SqlCompleter::new()));
loop {
let prompt = state.prompt();
let readline = rl.readline(&prompt);
match readline {
Ok(line) => {
let line = line.trim();
if line.is_empty() {
continue;
}
// 添加到历史记录
let _ = rl.add_history_entry(line);
// 处理命令
if let Err(e) = handle_command(&pool, &mut state, &executor, line) {
eprintln!("Error: {}", e);
}
}
Err(ReadlineError::Interrupted) => {
println!("^C");
continue;
}
Err(ReadlineError::Eof) => {
println!("Bye");
break;
}
Err(err) => {
eprintln!("Error: {}", err);
break;
}
}
}
Ok(())
}
/// 打印欢迎信息
fn print_welcome(pool: &Pool, config: &DbConfig) -> Result<()> {
println!();
println!("Welcome to MySQL CLI (Rust). Commands end with ;");
let mut conn = pool.get_conn()?;
let version = db::get_server_version(&mut conn);
println!("Server version: {}", version);
println!("Connected to: {}", config);
println!();
println!("Commands:");
println!(" use <database> - Switch database");
println!(" status - Show connection status");
println!(" source <file> - Execute SQL from file");
println!(" format <type> - Set output format (table, csv, json, vertical)");
println!(" output <file> - Set output file");
println!(" exit/quit - Exit the program");
println!();
Ok(())
}
/// 处理命令
fn handle_command(pool: &Pool, state: &mut ReplState, executor: &Executor, line: &str) -> Result<()> {
let lower_line = line.to_lowercase();
// 处理特殊命令
match lower_line.as_str() {
"exit" | "quit" => {
println!("Bye");
std::process::exit(0);
}
"status" => {
return show_status(pool, state);
}
_ => {}
}
if lower_line.starts_with("use ") {
return use_database(pool, state, &line[4..]);
}
if lower_line.starts_with("source ") {
return source_file(executor, &line[7..]);
}
if lower_line.starts_with("format ") {
return set_format(state, &line[7..]);
}
if lower_line.starts_with("output ") {
return set_output(state, &line[7..]);
}
// 执行 SQL
execute_sql(pool, state, line)
}
/// 显示状态
fn show_status(pool: &Pool, state: &ReplState) -> Result<()> {
println!();
println!("Connection Status:");
println!(" Host: {}:{}", state.config.host, state.config.port);
println!(" User: {}", state.config.username);
println!(" Database: {}", state.config.database);
let mut conn = pool.get_conn()?;
let version = db::get_server_version(&mut conn);
println!(" Server Version: {}", version);
if let Some(current_db) = db::get_current_database(&mut conn) {
println!(" Current Database: {}", current_db);
}
println!();
Ok(())
}
/// 切换数据库
fn use_database(pool: &Pool, state: &mut ReplState, db_name: &str) -> Result<()> {
let db_name = db_name.trim().trim_end_matches(';').trim();
if db_name.is_empty() {
anyhow::bail!("database name is required");
}
let mut conn = pool.get_conn()?;
conn.query_iter(format!("USE `{}`", db_name))?;
state.config.database = db_name.to_string();
println!("Database changed");
Ok(())
}
/// 从文件执行 SQL
fn source_file(executor: &Executor, filename: &str) -> Result<()> {
let filename = filename.trim().trim_end_matches(';').trim();
if filename.is_empty() {
anyhow::bail!("filename is required");
}
executor.execute_from_file(filename)?;
println!();
Ok(())
}
/// 设置输出格式
fn set_format(state: &mut ReplState, format: &str) -> Result<()> {
let format = format.trim().trim_end_matches(';').trim();
state.format = format.parse()?;
println!("Output format set to {}", format);
println!();
Ok(())
}
/// 设置输出文件
fn set_output(state: &mut ReplState, output: &str) -> Result<()> {
let output = output.trim().trim_end_matches(';').trim();
if output.is_empty() {
state.output = None;
println!("Output reset to stdout");
} else {
state.output = Some(output.to_string());
println!("Output set to {}", output);
}
println!();
Ok(())
}
/// 执行 SQL支持多行
fn execute_sql(pool: &Pool, state: &ReplState, line: &str) -> Result<()> {
let full_query = line.to_string();
let start = Instant::now();
let executor = Executor::new(pool.clone(), state.format, state.output.clone());
executor.execute(&full_query)?;
let elapsed = start.elapsed();
if elapsed.as_millis() > 100 {
println!();
println!("({:.2} sec)", elapsed.as_secs_f64());
} else {
println!();
println!("({:.3} sec)", elapsed.as_secs_f64());
}
println!();
Ok(())
}