feat: add AI Explain Query and Fix Error via Ollama

Extract shared call_ollama_chat helper from generate_sql to reuse
settings loading and Ollama API call logic. Add two new AI commands:
- explain_sql: explains what a SQL query does in plain language
- fix_sql_error: suggests corrected SQL based on the error and schema

UI additions: "AI Explain" toolbar button, "Explain" and "Fix with AI"
action buttons on query errors, inline explanation display in results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 18:48:39 +03:00
parent c001ad597f
commit 3ad0ee5cc3
6 changed files with 268 additions and 33 deletions

View File

@@ -73,16 +73,13 @@ pub async fn list_ollama_models(ollama_url: String) -> TuskResult<Vec<OllamaMode
Ok(tags.models)
}
#[tauri::command]
pub async fn generate_sql(
app: AppHandle,
state: State<'_, Arc<AppState>>,
connection_id: String,
prompt: String,
async fn call_ollama_chat(
app: &AppHandle,
system_prompt: String,
user_content: String,
) -> TuskResult<String> {
// Load AI settings
let settings = {
let path = get_ai_settings_path(&app)?;
let path = get_ai_settings_path(app)?;
if !path.exists() {
return Err(TuskError::Ai(
"No AI model selected. Open AI settings to choose a model.".to_string(),
@@ -98,24 +95,6 @@ pub async fn generate_sql(
));
}
// Build schema context
let schema_text = build_schema_context(&state, &connection_id).await?;
let system_prompt = format!(
"You are a PostgreSQL SQL generator. Given the database schema below and a natural language request, \
output ONLY a valid PostgreSQL SQL query. Do not include any explanation, markdown formatting, \
or code fences. Output raw SQL only.\n\n\
RULES:\n\
- Use FK relationships for correct JOIN conditions.\n\
- timestamp - timestamp = interval. To get a number use EXTRACT(EPOCH FROM (ts1 - ts2)).\n\
- interval cannot be cast to numeric directly.\n\
- When using UNION/UNION ALL, ensure matching column types; cast enums to text if they differ.\n\
- Use COALESCE for nullable columns in aggregations when appropriate.\n\
- Prefer LEFT JOIN when the related row may not exist.\n\n\
DATABASE SCHEMA:\n{}",
schema_text
);
let request = OllamaChatRequest {
model: settings.model,
messages: vec![
@@ -125,7 +104,7 @@ pub async fn generate_sql(
},
OllamaChatMessage {
role: "user".to_string(),
content: prompt,
content: user_content,
},
],
stream: false,
@@ -162,8 +141,89 @@ pub async fn generate_sql(
.await
.map_err(|e| TuskError::Ai(format!("Failed to parse Ollama response: {}", e)))?;
let sql = clean_sql_response(&chat_resp.message.content);
Ok(sql)
Ok(chat_resp.message.content)
}
#[tauri::command]
pub async fn generate_sql(
app: AppHandle,
state: State<'_, Arc<AppState>>,
connection_id: String,
prompt: String,
) -> TuskResult<String> {
let schema_text = build_schema_context(&state, &connection_id).await?;
let system_prompt = format!(
"You are a PostgreSQL SQL generator. Given the database schema below and a natural language request, \
output ONLY a valid PostgreSQL SQL query. Do not include any explanation, markdown formatting, \
or code fences. Output raw SQL only.\n\n\
RULES:\n\
- Use FK relationships for correct JOIN conditions.\n\
- timestamp - timestamp = interval. To get a number use EXTRACT(EPOCH FROM (ts1 - ts2)).\n\
- interval cannot be cast to numeric directly.\n\
- When using UNION/UNION ALL, ensure matching column types; cast enums to text if they differ.\n\
- Use COALESCE for nullable columns in aggregations when appropriate.\n\
- Prefer LEFT JOIN when the related row may not exist.\n\n\
DATABASE SCHEMA:\n{}",
schema_text
);
let raw = call_ollama_chat(&app, system_prompt, prompt).await?;
Ok(clean_sql_response(&raw))
}
#[tauri::command]
pub async fn explain_sql(
app: AppHandle,
state: State<'_, Arc<AppState>>,
connection_id: String,
sql: String,
) -> TuskResult<String> {
let schema_text = build_schema_context(&state, &connection_id).await?;
let system_prompt = format!(
"You are a PostgreSQL expert. Explain what this SQL query does in clear, concise language. \
Focus on the business logic, mention the tables, joins, and filters used. \
Use short paragraphs or bullet points.\n\n\
DATABASE SCHEMA:\n{}",
schema_text
);
call_ollama_chat(&app, system_prompt, sql).await
}
#[tauri::command]
pub async fn fix_sql_error(
app: AppHandle,
state: State<'_, Arc<AppState>>,
connection_id: String,
sql: String,
error_message: String,
) -> TuskResult<String> {
let schema_text = build_schema_context(&state, &connection_id).await?;
let system_prompt = format!(
"You are a PostgreSQL expert. Fix the SQL query based on the error message. \
Output ONLY the corrected valid PostgreSQL SQL. Do not include any explanation, \
markdown formatting, or code fences. Output raw SQL only.\n\n\
RULES:\n\
- Use FK relationships for correct JOIN conditions.\n\
- timestamp - timestamp = interval. To get a number use EXTRACT(EPOCH FROM (ts1 - ts2)).\n\
- interval cannot be cast to numeric directly.\n\
- When using UNION/UNION ALL, ensure matching column types; cast enums to text if they differ.\n\
- Use COALESCE for nullable columns in aggregations when appropriate.\n\
- Prefer LEFT JOIN when the related row may not exist.\n\n\
DATABASE SCHEMA:\n{}",
schema_text
);
let user_content = format!(
"Original SQL:\n{}\n\nError:\n{}",
sql, error_message
);
let raw = call_ollama_chat(&app, system_prompt, user_content).await?;
Ok(clean_sql_response(&raw))
}
async fn build_schema_context(

View File

@@ -94,6 +94,8 @@ pub fn run() {
commands::ai::save_ai_settings,
commands::ai::list_ollama_models,
commands::ai::generate_sql,
commands::ai::explain_sql,
commands::ai::fix_sql_error,
// lookup
commands::lookup::entity_lookup,
])