use crate::engine::Engine;
use crate::error::{ErrorCode, McpError};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
pub const SAVED_QUERIES_TABLE: &str = "_hyperdb_saved_queries";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SavedQuery {
pub name: String,
pub sql: String,
pub description: Option<String>,
pub created_at: DateTime<Utc>,
}
impl SavedQuery {
#[must_use]
pub fn to_json(&self) -> Value {
serde_json::json!({
"name": self.name,
"sql": self.sql,
"description": self.description,
"created_at": self.created_at.to_rfc3339(),
})
}
}
pub trait SavedQueryStore: Send + Sync {
fn save(&self, engine: Option<&Engine>, query: SavedQuery) -> Result<(), McpError>;
fn get(&self, engine: Option<&Engine>, name: &str) -> Result<Option<SavedQuery>, McpError>;
fn list(&self, engine: Option<&Engine>) -> Result<Vec<SavedQuery>, McpError>;
fn delete(&self, engine: Option<&Engine>, name: &str) -> Result<bool, McpError>;
}
#[derive(Debug, Default)]
pub struct SessionStore {
inner: Mutex<HashMap<String, SavedQuery>>,
}
impl SessionStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
impl SavedQueryStore for SessionStore {
fn save(&self, _engine: Option<&Engine>, query: SavedQuery) -> Result<(), McpError> {
let mut guard = self
.inner
.lock()
.map_err(|_| McpError::new(ErrorCode::InternalError, "SessionStore lock poisoned"))?;
if guard.contains_key(&query.name) {
return Err(McpError::new(
ErrorCode::InvalidArgument,
format!(
"A saved query named '{}' already exists. Delete it first with \
delete_query if you intend to overwrite.",
query.name
),
));
}
guard.insert(query.name.clone(), query);
Ok(())
}
fn get(&self, _engine: Option<&Engine>, name: &str) -> Result<Option<SavedQuery>, McpError> {
let guard = self
.inner
.lock()
.map_err(|_| McpError::new(ErrorCode::InternalError, "SessionStore lock poisoned"))?;
Ok(guard.get(name).cloned())
}
fn list(&self, _engine: Option<&Engine>) -> Result<Vec<SavedQuery>, McpError> {
let guard = self
.inner
.lock()
.map_err(|_| McpError::new(ErrorCode::InternalError, "SessionStore lock poisoned"))?;
let mut out: Vec<SavedQuery> = guard.values().cloned().collect();
out.sort_by(|a, b| a.name.cmp(&b.name));
Ok(out)
}
fn delete(&self, _engine: Option<&Engine>, name: &str) -> Result<bool, McpError> {
let mut guard = self
.inner
.lock()
.map_err(|_| McpError::new(ErrorCode::InternalError, "SessionStore lock poisoned"))?;
Ok(guard.remove(name).is_some())
}
}
#[derive(Debug, Default)]
pub struct WorkspaceStore {
initialized: Mutex<bool>,
}
impl WorkspaceStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
fn qualified_table() -> String {
format!(
"\"{}\".\"public\".\"{}\"",
Engine::PERSISTENT_ALIAS,
SAVED_QUERIES_TABLE
)
}
fn ensure_table(&self, engine: &Engine) -> Result<(), McpError> {
let mut flag = self
.initialized
.lock()
.map_err(|_| McpError::new(ErrorCode::InternalError, "WorkspaceStore lock poisoned"))?;
if *flag {
return Ok(());
}
let ddl = format!(
"CREATE TABLE IF NOT EXISTS {table} (\
name TEXT NOT NULL, \
sql TEXT NOT NULL, \
description TEXT, \
created_at TIMESTAMP NOT NULL\
)",
table = Self::qualified_table()
);
engine.execute_command(&ddl)?;
*flag = true;
Ok(())
}
}
fn sql_literal(s: &str) -> String {
format!("'{}'", s.replace('\'', "''"))
}
fn row_to_saved_query(row: &Value) -> Result<SavedQuery, McpError> {
let name = row
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| {
McpError::new(
ErrorCode::InternalError,
"_hyperdb_saved_queries row missing 'name'",
)
})?
.to_string();
let sql = row
.get("sql")
.and_then(|v| v.as_str())
.ok_or_else(|| {
McpError::new(
ErrorCode::InternalError,
"_hyperdb_saved_queries row missing 'sql'",
)
})?
.to_string();
let description = row
.get("description")
.and_then(|v| v.as_str())
.map(String::from);
let created_at_str = row.get("created_at").and_then(|v| v.as_str()).unwrap_or("");
let created_at = DateTime::parse_from_rfc3339(created_at_str)
.map(|d| d.with_timezone(&Utc))
.or_else(|_| {
chrono::NaiveDateTime::parse_from_str(created_at_str, "%Y-%m-%d %H:%M:%S%.f")
.or_else(|_| {
chrono::NaiveDateTime::parse_from_str(created_at_str, "%Y-%m-%d %H:%M:%S")
})
.map(|ndt| ndt.and_utc())
.map_err(|e| {
McpError::new(
ErrorCode::InternalError,
format!("Could not parse created_at '{created_at_str}': {e}"),
)
})
})?;
Ok(SavedQuery {
name,
sql,
description,
created_at,
})
}
impl SavedQueryStore for WorkspaceStore {
fn save(&self, engine: Option<&Engine>, query: SavedQuery) -> Result<(), McpError> {
let engine = engine.ok_or_else(|| {
McpError::new(
ErrorCode::InternalError,
"WorkspaceStore requires an engine handle",
)
})?;
self.ensure_table(engine)?;
let table = Self::qualified_table();
let existing_sql = format!(
"SELECT name FROM {table} WHERE name = {}",
sql_literal(&query.name)
);
let rows = engine.execute_query_to_json(&existing_sql)?;
if !rows.is_empty() {
return Err(McpError::new(
ErrorCode::InvalidArgument,
format!(
"A saved query named '{}' already exists. Delete it first with \
delete_query if you intend to overwrite.",
query.name
),
));
}
let description_sql = match &query.description {
Some(d) => sql_literal(d),
None => "NULL".into(),
};
let insert_sql = format!(
"INSERT INTO {table} (name, sql, description, created_at) \
VALUES ({name}, {sql}, {desc}, TIMESTAMP {ts})",
name = sql_literal(&query.name),
sql = sql_literal(&query.sql),
desc = description_sql,
ts = sql_literal(&query.created_at.format("%Y-%m-%d %H:%M:%S%.6f").to_string()),
);
engine.execute_command(&insert_sql)?;
Ok(())
}
fn get(&self, engine: Option<&Engine>, name: &str) -> Result<Option<SavedQuery>, McpError> {
let engine = engine.ok_or_else(|| {
McpError::new(
ErrorCode::InternalError,
"WorkspaceStore requires an engine handle",
)
})?;
self.ensure_table(engine)?;
let sql = format!(
"SELECT name, sql, description, created_at \
FROM {table} WHERE name = {}",
sql_literal(name),
table = Self::qualified_table(),
);
let rows = engine.execute_query_to_json(&sql)?;
match rows.first() {
Some(row) => Ok(Some(row_to_saved_query(row)?)),
None => Ok(None),
}
}
fn list(&self, engine: Option<&Engine>) -> Result<Vec<SavedQuery>, McpError> {
let engine = engine.ok_or_else(|| {
McpError::new(
ErrorCode::InternalError,
"WorkspaceStore requires an engine handle",
)
})?;
self.ensure_table(engine)?;
let sql = format!(
"SELECT name, sql, description, created_at \
FROM {table} ORDER BY name",
table = Self::qualified_table(),
);
let rows = engine.execute_query_to_json(&sql)?;
rows.iter().map(row_to_saved_query).collect()
}
fn delete(&self, engine: Option<&Engine>, name: &str) -> Result<bool, McpError> {
let engine = engine.ok_or_else(|| {
McpError::new(
ErrorCode::InternalError,
"WorkspaceStore requires an engine handle",
)
})?;
self.ensure_table(engine)?;
let sql = format!(
"DELETE FROM {table} WHERE name = {}",
sql_literal(name),
table = Self::qualified_table(),
);
let affected = engine.execute_command(&sql)?;
Ok(affected > 0)
}
}
#[must_use]
pub fn build_store(workspace_path: Option<&str>) -> Arc<dyn SavedQueryStore> {
if workspace_path.is_some() {
Arc::new(WorkspaceStore::new())
} else {
Arc::new(SessionStore::new())
}
}