use once_cell::sync::Lazy;
use regex::Regex;
static RE_CTAS: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?is)\bCREATE\s+TABLE\b.*?\bAS\b.*?;").unwrap());
static RE_WITH: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?is)\bWITH\b\s+.*?;").unwrap());
static RE_SELECT: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?is)\bSELECT\b\s+.*?;").unwrap());
static RE_SQL_FENCE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?is)```sql\s*\n(.*?)```").unwrap());
static RE_ANY_FENCE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?is)```(.*?)```").unwrap());
pub fn extract_sql(response: &str) -> String {
if let Some(m) = RE_CTAS.find_iter(response).last() {
return m.as_str().trim().to_string();
}
if let Some(m) = RE_WITH.find_iter(response).last() {
return m.as_str().trim().to_string();
}
if let Some(m) = RE_SELECT.find_iter(response).last() {
return m.as_str().trim().to_string();
}
if let Some(c) = RE_SQL_FENCE.captures_iter(response).last() {
return c[1].trim().to_string();
}
if let Some(c) = RE_ANY_FENCE.captures_iter(response).last() {
return c[1].trim().to_string();
}
response.trim().to_string()
}
pub fn is_sql_valid(sql: &str) -> bool {
let upper = sql.trim_start().to_uppercase();
upper.starts_with("SELECT") || upper.starts_with("WITH")
}
pub fn is_read_only(sql: &str) -> bool {
if !regex_read_only(sql) {
return false;
}
!matches!(crate::schema::is_read_only_ast(sql), Some(false))
}
fn regex_read_only(sql: &str) -> bool {
let trimmed = sql.trim_start();
let upper = trimmed.to_uppercase();
if upper.starts_with("SELECT") {
return true;
}
if upper.starts_with("WITH") {
static RE_WRITE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|TRUNCATE|REPLACE|MERGE|GRANT|REVOKE)\b")
.unwrap()
});
return !RE_WRITE.is_match(trimmed);
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strips_sql_fence() {
let r = "Here you go:\n```sql\nSELECT * FROM users;\n```";
assert_eq!(extract_sql(r), "SELECT * FROM users;");
}
#[test]
fn plain_select() {
assert_eq!(extract_sql("SELECT 1 FROM t;"), "SELECT 1 FROM t;");
}
#[test]
fn validity() {
assert!(is_sql_valid("SELECT 1"));
assert!(is_sql_valid(" with x as (...) select 1"));
assert!(!is_sql_valid("DROP TABLE users"));
}
#[test]
fn read_only_gate_blocks_writes() {
assert!(is_read_only("SELECT * FROM orders"));
assert!(is_read_only(" select 1"));
assert!(is_read_only("WITH t AS (SELECT 1) SELECT * FROM t"));
assert!(is_read_only("SELECT * FROM orders WHERE status = 'deleted'"));
assert!(!is_read_only("DROP TABLE users"));
assert!(!is_read_only("DELETE FROM orders"));
assert!(!is_read_only("UPDATE orders SET total = 0"));
assert!(!is_read_only("INSERT INTO orders VALUES (1)"));
assert!(!is_read_only("ALTER TABLE orders ADD COLUMN x INT"));
assert!(!is_read_only("TRUNCATE orders"));
assert!(!is_read_only("WITH t AS (DELETE FROM orders RETURNING *) SELECT * FROM t"));
assert!(!is_read_only("SELECT 1; DROP TABLE users"));
}
}