use serde::Deserialize;
use serde_json::{Value, json};
use crate::error::ToolError;
use crate::protocol::ServerState;
use crate::tools::{TOOL_OUTPUT_CAP_BYTES, value_to_json};
const DEFAULT_LIMIT: u64 = 100;
const HARD_CAP_LIMIT: u64 = 1000;
pub fn metadata() -> Value {
json!({
"name": "query",
"description": "Execute a SELECT query against the database and return matching \
rows as a JSON array of objects (key = column name). \
SELECT-only — for INSERT/UPDATE/DELETE/CREATE use the \
`execute` tool instead. Add `LIMIT` clauses on large tables; \
the tool also caps results at `limit` (default 100, max 1000) \
and notes any truncation in the response.",
"inputSchema": {
"type": "object",
"properties": {
"sql": {
"type": "string",
"description": "A single SELECT statement.",
},
"limit": {
"type": "integer",
"description": "Max rows to return (1..=1000, default 100). \
This is enforced at the tool layer in addition \
to any LIMIT clause in the SQL itself.",
"minimum": 1,
"maximum": 1000,
},
},
"required": ["sql"],
"additionalProperties": false,
}
})
}
#[derive(Deserialize)]
struct Args {
sql: String,
#[serde(default)]
limit: Option<u64>,
}
pub fn handle(args: Value, state: &mut ServerState) -> Result<String, ToolError> {
let args: Args = serde_json::from_value(args)
.map_err(|e| ToolError::new(format!("invalid arguments: {e}")))?;
let trimmed = args.sql.trim_start();
let lowered = trimmed.to_ascii_lowercase();
if !lowered.starts_with("select") {
return Err(ToolError::new(
"the `query` tool only accepts SELECT statements. \
Use the `execute` tool for INSERT, UPDATE, DELETE, CREATE, etc."
.to_string(),
));
}
let limit = args.limit.unwrap_or(DEFAULT_LIMIT).min(HARD_CAP_LIMIT) as usize;
let stmt = state.conn.prepare(&args.sql)?;
let mut rows = stmt.query()?;
let columns = rows.columns().to_vec();
let mut out: Vec<Value> = Vec::new();
let mut total_seen: usize = 0;
let mut byte_truncated = false;
let mut size_estimate: usize = 0;
while let Some(row) = rows.next()? {
total_seen += 1;
if out.len() >= limit {
continue;
}
let mut obj = serde_json::Map::with_capacity(columns.len());
for (i, col) in columns.iter().enumerate() {
let v: sqlrite::Value = row.get(i)?;
let json_val = value_to_json(&v);
size_estimate += col.len() + 8 + json_val.to_string().len();
obj.insert(col.clone(), json_val);
}
if size_estimate > TOOL_OUTPUT_CAP_BYTES {
byte_truncated = true;
break;
}
out.push(Value::Object(obj));
}
if byte_truncated {
while rows.next()?.is_some() {
total_seen += 1;
}
}
let kept = out.len();
let truncated = byte_truncated || (total_seen > kept);
let mut result = json!({
"columns": columns,
"rows": out,
"row_count": kept,
});
if truncated {
let reason = if byte_truncated {
format!(
"response truncated at {} bytes ({} of {} rows shown)",
TOOL_OUTPUT_CAP_BYTES, kept, total_seen
)
} else {
format!(
"row limit {} reached ({} of {} rows shown)",
limit, kept, total_seen
)
};
result["truncated"] = json!(true);
result["truncation_reason"] = json!(reason);
result["total_seen"] = json!(total_seen);
}
serde_json::to_string_pretty(&result)
.map_err(|e| ToolError::new(format!("internal: failed to serialize rows: {e}")))
}