Robust SQL extraction: handles explanations, markdown, prefixes
clean_sql now uses 3 strategies in priority order: 1. Extract from ```sql...``` markdown blocks 2. Find first SELECT/WITH/INSERT statement in text 3. Strip leading "sql" keyword fallback Tested against 5 real model output patterns: - Clean SQL ✓ - "sql" prefixed ✓ - Markdown fenced ✓ - Explanation before ```sql block ✓ - Explanation with SELECT buried in text ✓ Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+37
-13
@@ -1025,23 +1025,47 @@ fn ResultsTable(response: QueryResponse) -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clean AI-generated SQL: strip markdown fences, leading "sql" keyword, explanations.
|
||||
/// Clean AI-generated SQL: extract only the SQL query, strip everything else.
|
||||
fn clean_sql(raw: &str) -> String {
|
||||
let mut s = raw.trim().to_string();
|
||||
// Remove markdown code fences
|
||||
s = s.trim_start_matches("```sql").trim_start_matches("```").trim_end_matches("```").trim().to_string();
|
||||
// Remove leading "sql" keyword on its own line
|
||||
let lines: Vec<&str> = s.lines().collect();
|
||||
if let Some(first) = lines.first() {
|
||||
if first.trim().eq_ignore_ascii_case("sql") || first.trim().eq_ignore_ascii_case("sql;") {
|
||||
s = lines[1..].join("\n").trim().to_string();
|
||||
let s = raw.trim();
|
||||
|
||||
// Strategy 1: If there's a ```sql...``` block, extract just that
|
||||
if let Some(start) = s.find("```sql") {
|
||||
let after = &s[start + 6..];
|
||||
if let Some(end) = after.find("```") {
|
||||
return after[..end].trim().to_string();
|
||||
}
|
||||
}
|
||||
// If the model added explanation after the SQL, keep only up to the first semicolon line
|
||||
if let Some(pos) = s.find(";\n\n") {
|
||||
s = s[..pos + 1].to_string();
|
||||
if let Some(start) = s.find("```") {
|
||||
let after = &s[start + 3..];
|
||||
if let Some(end) = after.find("```") {
|
||||
let inner = after[..end].trim();
|
||||
// Skip leading "sql" keyword
|
||||
let inner = inner.strip_prefix("sql").map(|s| s.trim_start()).unwrap_or(inner);
|
||||
return inner.to_string();
|
||||
}
|
||||
}
|
||||
s
|
||||
|
||||
// Strategy 2: Find the first SELECT/WITH/INSERT/UPDATE/DELETE statement
|
||||
let upper = s.to_uppercase();
|
||||
for keyword in &["SELECT", "WITH", "INSERT", "UPDATE", "DELETE"] {
|
||||
if let Some(pos) = upper.find(keyword) {
|
||||
let sql_part = &s[pos..];
|
||||
// Take up to the first semicolon (or end)
|
||||
let end = sql_part.find(';').map(|p| p + 1).unwrap_or(sql_part.len());
|
||||
return sql_part[..end].trim().to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: Strip leading "sql" and clean up
|
||||
let mut result = s.to_string();
|
||||
let lines: Vec<&str> = result.lines().collect();
|
||||
if let Some(first) = lines.first() {
|
||||
if first.trim().eq_ignore_ascii_case("sql") {
|
||||
result = lines[1..].join("\n").trim().to_string();
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn format_cell(val: Option<&serde_json::Value>) -> String {
|
||||
|
||||
Reference in New Issue
Block a user