mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-17 08:34:19 +01:00
746 lines
28 KiB
Rust
746 lines
28 KiB
Rust
use anyhow::Result;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::{json, Value};
|
|
use std::io::{self, BufRead, BufReader, Write};
|
|
use std::path::PathBuf;
|
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
use std::sync::mpsc;
|
|
use std::sync::{Arc, Mutex};
|
|
use std::thread;
|
|
use std::time::Duration;
|
|
use turso_core::{Connection, Database, DatabaseOpts, OpenFlags, StepResult, Value as DbValue};
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct JsonRpcRequest {
|
|
jsonrpc: String,
|
|
id: Option<Value>,
|
|
method: String,
|
|
params: Option<Value>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct JsonRpcResponse {
|
|
jsonrpc: String,
|
|
id: Option<Value>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
result: Option<Value>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
error: Option<JsonRpcError>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct JsonRpcError {
|
|
code: i32,
|
|
message: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
data: Option<Value>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct InitializeRequest {
|
|
#[serde(rename = "protocolVersion")]
|
|
protocol_version: String,
|
|
capabilities: Value,
|
|
#[serde(rename = "clientInfo")]
|
|
client_info: Value,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct CallToolRequest {
|
|
name: String,
|
|
arguments: Option<Value>,
|
|
}
|
|
|
|
pub struct TursoMcpServer {
|
|
conn: Arc<Mutex<Arc<Connection>>>,
|
|
interrupt_count: Arc<AtomicUsize>,
|
|
current_db_path: Arc<Mutex<Option<String>>>,
|
|
}
|
|
|
|
impl TursoMcpServer {
|
|
pub fn new(conn: Arc<Connection>, interrupt_count: Arc<AtomicUsize>) -> Self {
|
|
Self {
|
|
conn: Arc::new(Mutex::new(conn)),
|
|
interrupt_count,
|
|
current_db_path: Arc::new(Mutex::new(None)),
|
|
}
|
|
}
|
|
|
|
pub fn run(&self) -> Result<()> {
|
|
let stdout = io::stdout();
|
|
let mut stdout_lock = stdout.lock();
|
|
|
|
// Create a channel to receive lines from stdin
|
|
let (tx, rx) = mpsc::channel();
|
|
|
|
// Spawn a thread to read from stdin
|
|
thread::spawn(move || {
|
|
let stdin = io::stdin();
|
|
let reader = BufReader::new(stdin);
|
|
|
|
for line in reader.lines() {
|
|
match line {
|
|
Ok(line) => {
|
|
if tx.send(Ok(line)).is_err() {
|
|
break; // Main thread has dropped the receiver
|
|
}
|
|
}
|
|
Err(e) => {
|
|
let _ = tx.send(Err(e));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
loop {
|
|
// Check if we've been interrupted
|
|
if self.interrupt_count.load(Ordering::SeqCst) > 0 {
|
|
eprintln!("MCP server interrupted, shutting down...");
|
|
break;
|
|
}
|
|
|
|
// Try to receive a line with a timeout so we can check for interruption
|
|
match rx.recv_timeout(Duration::from_millis(100)) {
|
|
Ok(Ok(line)) => {
|
|
if line.trim().is_empty() {
|
|
continue;
|
|
}
|
|
|
|
let request: JsonRpcRequest = match serde_json::from_str(&line) {
|
|
Ok(req) => req,
|
|
Err(e) => {
|
|
eprintln!("Failed to parse JSON-RPC request: {e}");
|
|
continue;
|
|
}
|
|
};
|
|
|
|
let response = self.handle_request(request);
|
|
// Don't send a response for notifications (when id is None)
|
|
if response.id.is_some() || response.error.is_some() {
|
|
let response_json = serde_json::to_string(&response)?;
|
|
writeln!(stdout_lock, "{response_json}")?;
|
|
stdout_lock.flush()?;
|
|
}
|
|
}
|
|
Ok(Err(_)) => {
|
|
// Error reading from stdin
|
|
break;
|
|
}
|
|
Err(mpsc::RecvTimeoutError::Timeout) => {
|
|
// Timeout - continue loop to check for interruption
|
|
continue;
|
|
}
|
|
Err(mpsc::RecvTimeoutError::Disconnected) => {
|
|
// Stdin thread has finished (EOF)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn handle_request(&self, request: JsonRpcRequest) -> JsonRpcResponse {
|
|
// Check if this is a notification (no id field means it's a notification)
|
|
// Notifications should not receive a response according to JSON-RPC spec
|
|
if request.id.is_none() {
|
|
// For notifications, we return a special response that the caller should ignore
|
|
return JsonRpcResponse {
|
|
jsonrpc: "2.0".to_string(),
|
|
id: None,
|
|
result: None,
|
|
error: None,
|
|
};
|
|
}
|
|
|
|
match request.method.as_str() {
|
|
"initialize" => self.handle_initialize(request),
|
|
"tools/list" => self.handle_list_tools(request),
|
|
"tools/call" => self.handle_call_tool(request),
|
|
_ => JsonRpcResponse {
|
|
jsonrpc: "2.0".to_string(),
|
|
id: request.id,
|
|
result: None,
|
|
error: Some(JsonRpcError {
|
|
code: -32601,
|
|
message: "Method not found".to_string(),
|
|
data: None,
|
|
}),
|
|
},
|
|
}
|
|
}
|
|
|
|
fn handle_initialize(&self, request: JsonRpcRequest) -> JsonRpcResponse {
|
|
JsonRpcResponse {
|
|
jsonrpc: "2.0".to_string(),
|
|
id: request.id,
|
|
result: Some(json!({
|
|
"protocolVersion": "2024-11-05",
|
|
"capabilities": {
|
|
"tools": {}
|
|
},
|
|
"serverInfo": {
|
|
"name": "turso-mcp",
|
|
"version": "1.0.0"
|
|
}
|
|
})),
|
|
error: None,
|
|
}
|
|
}
|
|
|
|
fn handle_list_tools(&self, request: JsonRpcRequest) -> JsonRpcResponse {
|
|
JsonRpcResponse {
|
|
jsonrpc: "2.0".to_string(),
|
|
id: request.id,
|
|
result: Some(json!({
|
|
"tools": [
|
|
{
|
|
"name": "open_database",
|
|
"description": "Open or create a database file. Creates parent directories if needed.",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"path": {
|
|
"type": "string",
|
|
"description": "Path to the database file (absolute or relative). Use ':memory:' for in-memory database."
|
|
}
|
|
},
|
|
"required": ["path"]
|
|
}
|
|
},
|
|
{
|
|
"name": "current_database",
|
|
"description": "Get the path of the currently open database",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {},
|
|
"required": []
|
|
}
|
|
},
|
|
{
|
|
"name": "list_tables",
|
|
"description": "List all tables in the database",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {},
|
|
"required": []
|
|
}
|
|
},
|
|
{
|
|
"name": "describe_table",
|
|
"description": "Describe the structure of a specific table",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"table_name": {
|
|
"type": "string",
|
|
"description": "Name of the table to describe"
|
|
}
|
|
},
|
|
"required": ["table_name"]
|
|
}
|
|
},
|
|
{
|
|
"name": "execute_query",
|
|
"description": "Execute a read-only SELECT query",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {
|
|
"type": "string",
|
|
"description": "The SELECT query to execute"
|
|
}
|
|
},
|
|
"required": ["query"]
|
|
}
|
|
},
|
|
{
|
|
"name": "insert_data",
|
|
"description": "Insert new data into a table",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {
|
|
"type": "string",
|
|
"description": "The INSERT statement to execute"
|
|
}
|
|
},
|
|
"required": ["query"]
|
|
}
|
|
},
|
|
{
|
|
"name": "update_data",
|
|
"description": "Update existing data in a table",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {
|
|
"type": "string",
|
|
"description": "The UPDATE statement to execute"
|
|
}
|
|
},
|
|
"required": ["query"]
|
|
}
|
|
},
|
|
{
|
|
"name": "delete_data",
|
|
"description": "Delete data from a table",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {
|
|
"type": "string",
|
|
"description": "The DELETE statement to execute"
|
|
}
|
|
},
|
|
"required": ["query"]
|
|
}
|
|
},
|
|
{
|
|
"name": "schema_change",
|
|
"description": "Execute schema modification statements (CREATE TABLE, ALTER TABLE, DROP TABLE)",
|
|
"inputSchema": {
|
|
"type": "object",
|
|
"properties": {
|
|
"query": {
|
|
"type": "string",
|
|
"description": "The schema modification statement to execute"
|
|
}
|
|
},
|
|
"required": ["query"]
|
|
}
|
|
}
|
|
]
|
|
})),
|
|
error: None,
|
|
}
|
|
}
|
|
|
|
fn handle_call_tool(&self, request: JsonRpcRequest) -> JsonRpcResponse {
|
|
let tool_request: CallToolRequest = match request.params.as_ref() {
|
|
Some(params) => match serde_json::from_value(params.clone()) {
|
|
Ok(req) => req,
|
|
Err(e) => {
|
|
return JsonRpcResponse {
|
|
jsonrpc: "2.0".to_string(),
|
|
id: request.id,
|
|
result: None,
|
|
error: Some(JsonRpcError {
|
|
code: -32602,
|
|
message: format!("Invalid params: {e}"),
|
|
data: None,
|
|
}),
|
|
};
|
|
}
|
|
},
|
|
None => {
|
|
return JsonRpcResponse {
|
|
jsonrpc: "2.0".to_string(),
|
|
id: request.id,
|
|
result: None,
|
|
error: Some(JsonRpcError {
|
|
code: -32602,
|
|
message: "Missing params".to_string(),
|
|
data: None,
|
|
}),
|
|
};
|
|
}
|
|
};
|
|
|
|
let result = match tool_request.name.as_str() {
|
|
"open_database" => self.open_database(&tool_request.arguments),
|
|
"current_database" => self.current_database(),
|
|
"list_tables" => self.list_tables(),
|
|
"describe_table" => self.describe_table(&tool_request.arguments),
|
|
"execute_query" => self.execute_query(&tool_request.arguments),
|
|
"insert_data" => self.insert_data(&tool_request.arguments),
|
|
"update_data" => self.update_data(&tool_request.arguments),
|
|
"delete_data" => self.delete_data(&tool_request.arguments),
|
|
"schema_change" => self.schema_change(&tool_request.arguments),
|
|
_ => {
|
|
return JsonRpcResponse {
|
|
jsonrpc: "2.0".to_string(),
|
|
id: request.id,
|
|
result: None,
|
|
error: Some(JsonRpcError {
|
|
code: -32601,
|
|
message: format!("Unknown tool: {}", tool_request.name),
|
|
data: None,
|
|
}),
|
|
};
|
|
}
|
|
};
|
|
|
|
JsonRpcResponse {
|
|
jsonrpc: "2.0".to_string(),
|
|
id: request.id,
|
|
result: Some(json!({
|
|
"content": [{
|
|
"type": "text",
|
|
"text": result
|
|
}]
|
|
})),
|
|
error: None,
|
|
}
|
|
}
|
|
|
|
fn open_database(&self, arguments: &Option<Value>) -> String {
|
|
let path = match arguments {
|
|
Some(args) => match args.get("path") {
|
|
Some(Value::String(p)) => p.clone(),
|
|
_ => return "Missing or invalid path parameter".to_string(),
|
|
},
|
|
None => return "Missing path parameter".to_string(),
|
|
};
|
|
|
|
// Create parent directories if needed
|
|
if path != ":memory:" {
|
|
let db_path = PathBuf::from(&path);
|
|
if let Some(parent) = db_path.parent() {
|
|
if !parent.exists() {
|
|
if let Err(e) = std::fs::create_dir_all(parent) {
|
|
return format!("Failed to create parent directories: {e}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Open the new database connection
|
|
let conn = if path == ":memory:" || path.contains([':', '?', '&', '#']) {
|
|
match Connection::from_uri(&path, true, false, false, false, false) {
|
|
Ok((_io, c)) => c,
|
|
Err(e) => return format!("Failed to open database '{path}': {e}"),
|
|
}
|
|
} else {
|
|
match Database::open_new(
|
|
&path,
|
|
None::<&str>,
|
|
OpenFlags::default(),
|
|
DatabaseOpts::new().with_indexes(true),
|
|
None,
|
|
) {
|
|
Ok((_io, db)) => match db.connect() {
|
|
Ok(c) => c,
|
|
Err(e) => return format!("Failed to connect to database '{path}': {e}"),
|
|
},
|
|
Err(e) => return format!("Failed to open database '{path}': {e}"),
|
|
}
|
|
};
|
|
|
|
// Update the connection and path
|
|
*self.conn.lock().unwrap() = conn;
|
|
*self.current_db_path.lock().unwrap() = Some(path.clone());
|
|
|
|
format!("Successfully opened database: {path}")
|
|
}
|
|
|
|
fn current_database(&self) -> String {
|
|
match &*self.current_db_path.lock().unwrap() {
|
|
Some(path) => format!("Current database: {path}"),
|
|
None => "Current database: :memory: (default)".to_string(),
|
|
}
|
|
}
|
|
|
|
fn list_tables(&self) -> String {
|
|
let query = "SELECT name FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY 1";
|
|
|
|
let conn = self.conn.lock().unwrap().clone();
|
|
match conn.query(query) {
|
|
Ok(Some(mut rows)) => {
|
|
let mut tables = Vec::new();
|
|
|
|
loop {
|
|
match rows.step() {
|
|
Ok(StepResult::Row) => {
|
|
let row = rows.row().unwrap();
|
|
if let Ok(DbValue::Text(table)) = row.get::<&DbValue>(0) {
|
|
tables.push(table.to_string());
|
|
}
|
|
}
|
|
Ok(StepResult::IO) => {
|
|
if rows.run_once().is_err() {
|
|
break;
|
|
}
|
|
}
|
|
Ok(StepResult::Done) => break,
|
|
Ok(StepResult::Interrupt) => break,
|
|
Ok(StepResult::Busy) => {
|
|
return "Database is busy".to_string();
|
|
}
|
|
Err(e) => {
|
|
return format!("Error listing tables: {e}");
|
|
}
|
|
}
|
|
}
|
|
|
|
if tables.is_empty() {
|
|
"No tables found in the database".to_string()
|
|
} else {
|
|
tables.join(", ")
|
|
}
|
|
}
|
|
Ok(None) => "No results returned from the query".to_string(),
|
|
Err(e) => format!("Error querying database: {e}"),
|
|
}
|
|
}
|
|
|
|
fn describe_table(&self, arguments: &Option<Value>) -> String {
|
|
let table_name = match arguments {
|
|
Some(args) => match args.get("table_name") {
|
|
Some(Value::String(name)) => name,
|
|
_ => return "Missing or invalid table_name parameter".to_string(),
|
|
},
|
|
None => return "Missing table_name parameter".to_string(),
|
|
};
|
|
|
|
let query = format!("PRAGMA table_info({table_name})");
|
|
|
|
let conn = self.conn.lock().unwrap().clone();
|
|
match conn.query(&query) {
|
|
Ok(Some(mut rows)) => {
|
|
let mut columns = Vec::new();
|
|
|
|
loop {
|
|
match rows.step() {
|
|
Ok(StepResult::Row) => {
|
|
let row = rows.row().unwrap();
|
|
if let (
|
|
Ok(col_name),
|
|
Ok(col_type),
|
|
Ok(not_null),
|
|
Ok(default_value),
|
|
Ok(pk),
|
|
) = (
|
|
row.get::<&DbValue>(1),
|
|
row.get::<&DbValue>(2),
|
|
row.get::<&DbValue>(3),
|
|
row.get::<&DbValue>(4),
|
|
row.get::<&DbValue>(5),
|
|
) {
|
|
let default_str = if matches!(default_value, DbValue::Null) {
|
|
"".to_string()
|
|
} else {
|
|
format!("DEFAULT {default_value}")
|
|
};
|
|
|
|
columns.push(
|
|
format!(
|
|
"{} {} {} {} {}",
|
|
col_name,
|
|
col_type,
|
|
if matches!(not_null, DbValue::Integer(1)) {
|
|
"NOT NULL"
|
|
} else {
|
|
"NULL"
|
|
},
|
|
default_str,
|
|
if matches!(pk, DbValue::Integer(1)) {
|
|
"PRIMARY KEY"
|
|
} else {
|
|
""
|
|
}
|
|
)
|
|
.trim()
|
|
.to_string(),
|
|
);
|
|
}
|
|
}
|
|
Ok(StepResult::IO) => {
|
|
if rows.run_once().is_err() {
|
|
break;
|
|
}
|
|
}
|
|
Ok(StepResult::Done) => break,
|
|
Ok(StepResult::Interrupt) => break,
|
|
Ok(StepResult::Busy) => {
|
|
return "Database is busy".to_string();
|
|
}
|
|
Err(e) => {
|
|
return format!("Error describing table: {e}");
|
|
}
|
|
}
|
|
}
|
|
|
|
if columns.is_empty() {
|
|
format!("Table '{table_name}' not found")
|
|
} else {
|
|
format!("Table '{table_name}' columns:\n{}", columns.join("\n"))
|
|
}
|
|
}
|
|
Ok(None) => format!("Table '{table_name}' not found"),
|
|
Err(e) => format!("Error querying database: {e}"),
|
|
}
|
|
}
|
|
|
|
fn execute_query(&self, arguments: &Option<Value>) -> String {
|
|
let query = match arguments {
|
|
Some(args) => match args.get("query") {
|
|
Some(Value::String(q)) => q,
|
|
_ => return "Missing or invalid query parameter".to_string(),
|
|
},
|
|
None => return "Missing query parameter".to_string(),
|
|
};
|
|
|
|
// Basic validation to ensure it's a read-only query
|
|
let trimmed_query = query.trim().to_lowercase();
|
|
if !trimmed_query.starts_with("select") {
|
|
return "Only SELECT queries are allowed".to_string();
|
|
}
|
|
|
|
let conn = self.conn.lock().unwrap().clone();
|
|
match conn.query(query) {
|
|
Ok(Some(mut rows)) => {
|
|
let mut results = Vec::new();
|
|
|
|
// Get column names
|
|
let headers: Vec<String> = (0..rows.num_columns())
|
|
.map(|i| rows.get_column_name(i).to_string())
|
|
.collect();
|
|
|
|
// Get the data
|
|
loop {
|
|
match rows.step() {
|
|
Ok(StepResult::Row) => {
|
|
let row = rows.row().unwrap();
|
|
let mut row_data = Vec::new();
|
|
|
|
for value in row.get_values() {
|
|
row_data.push(value.to_string());
|
|
}
|
|
|
|
results.push(row_data);
|
|
}
|
|
Ok(StepResult::IO) => {
|
|
if rows.run_once().is_err() {
|
|
break;
|
|
}
|
|
}
|
|
Ok(StepResult::Done) => break,
|
|
Ok(StepResult::Interrupt) => break,
|
|
Ok(StepResult::Busy) => {
|
|
return "Database is busy".to_string();
|
|
}
|
|
Err(e) => {
|
|
return format!("Error executing query: {e}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Format results as text table
|
|
let mut output = String::new();
|
|
if !headers.is_empty() {
|
|
output.push_str(&headers.join(" | "));
|
|
output.push('\n');
|
|
output.push_str(&"-".repeat(headers.join(" | ").len()));
|
|
output.push('\n');
|
|
}
|
|
|
|
for row in results {
|
|
output.push_str(&row.join(" | "));
|
|
output.push('\n');
|
|
}
|
|
|
|
if output.is_empty() {
|
|
"No results returned from the query".to_string()
|
|
} else {
|
|
output
|
|
}
|
|
}
|
|
Ok(None) => "No results returned from the query".to_string(),
|
|
Err(e) => format!("Error executing query: {e}"),
|
|
}
|
|
}
|
|
|
|
fn insert_data(&self, arguments: &Option<Value>) -> String {
|
|
let query = match arguments {
|
|
Some(args) => match args.get("query") {
|
|
Some(Value::String(q)) => q,
|
|
_ => return "Missing or invalid query parameter".to_string(),
|
|
},
|
|
None => return "Missing query parameter".to_string(),
|
|
};
|
|
|
|
// Basic validation to ensure it's an INSERT query
|
|
let trimmed_query = query.trim().to_lowercase();
|
|
if !trimmed_query.starts_with("insert") {
|
|
return "Only INSERT statements are allowed".to_string();
|
|
}
|
|
|
|
let conn = self.conn.lock().unwrap().clone();
|
|
match conn.execute(query) {
|
|
Ok(()) => "INSERT successful.".to_string(),
|
|
Err(e) => format!("Error executing INSERT: {e}"),
|
|
}
|
|
}
|
|
|
|
fn update_data(&self, arguments: &Option<Value>) -> String {
|
|
let query = match arguments {
|
|
Some(args) => match args.get("query") {
|
|
Some(Value::String(q)) => q,
|
|
_ => return "Missing or invalid query parameter".to_string(),
|
|
},
|
|
None => return "Missing query parameter".to_string(),
|
|
};
|
|
|
|
// Basic validation to ensure it's an UPDATE query
|
|
let trimmed_query = query.trim().to_lowercase();
|
|
if !trimmed_query.starts_with("update") {
|
|
return "Only UPDATE statements are allowed".to_string();
|
|
}
|
|
|
|
let conn = self.conn.lock().unwrap().clone();
|
|
match conn.execute(query) {
|
|
Ok(()) => "UPDATE successful.".to_string(),
|
|
Err(e) => format!("Error executing UPDATE: {e}"),
|
|
}
|
|
}
|
|
|
|
fn delete_data(&self, arguments: &Option<Value>) -> String {
|
|
let query = match arguments {
|
|
Some(args) => match args.get("query") {
|
|
Some(Value::String(q)) => q,
|
|
_ => return "Missing or invalid query parameter".to_string(),
|
|
},
|
|
None => return "Missing query parameter".to_string(),
|
|
};
|
|
|
|
// Basic validation to ensure it's a DELETE query
|
|
let trimmed_query = query.trim().to_lowercase();
|
|
if !trimmed_query.starts_with("delete") {
|
|
return "Only DELETE statements are allowed".to_string();
|
|
}
|
|
|
|
let conn = self.conn.lock().unwrap().clone();
|
|
match conn.execute(query) {
|
|
Ok(()) => "DELETE successful.".to_string(),
|
|
Err(e) => format!("Error executing DELETE: {e}"),
|
|
}
|
|
}
|
|
|
|
fn schema_change(&self, arguments: &Option<Value>) -> String {
|
|
let query = match arguments {
|
|
Some(args) => match args.get("query") {
|
|
Some(Value::String(q)) => q,
|
|
_ => return "Missing or invalid query parameter".to_string(),
|
|
},
|
|
None => return "Missing query parameter".to_string(),
|
|
};
|
|
|
|
// Basic validation to ensure it's a schema modification query
|
|
let trimmed_query = query.trim().to_lowercase();
|
|
if !trimmed_query.starts_with("create")
|
|
&& !trimmed_query.starts_with("alter")
|
|
&& !trimmed_query.starts_with("drop")
|
|
{
|
|
return "Only CREATE, ALTER, and DROP statements are allowed".to_string();
|
|
}
|
|
|
|
let conn = self.conn.lock().unwrap().clone();
|
|
match conn.execute(query) {
|
|
Ok(()) => "Schema change successful.".to_string(),
|
|
Err(e) => format!("Error executing schema change: {e}"),
|
|
}
|
|
}
|
|
}
|