Spaces:
Runtime error
Runtime error
File size: 6,216 Bytes
37a6ee1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | import os
import sqlite3
import re
import time
import pandas as pd
from typing import Dict, Any
from backend import config
class SQLService:
def __init__(self):
print("[SQLService] Initializing SQLService singleton...")
def validate_sql(self, sql: str) -> bool:
"""
Validates that the SQL query is a SELECT statement and does not contain
forbidden modifying operations like INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, EXEC.
"""
print(f"[SQLService] Validating SQL: {sql}")
try:
if not sql or not isinstance(sql, str):
return False
sql_upper = sql.upper().strip()
# The SQL must be a SELECT statement
if "SELECT" not in sql_upper:
print("[SQLService] Validation failed: SQL does not contain SELECT keyword.")
return False
# Forbidden keywords pattern with word boundaries to avoid false positives (e.g. column name like 'create_date')
forbidden_pattern = r"\b(INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC)\b"
if re.search(forbidden_pattern, sql_upper):
print("[SQLService] Validation failed: SQL contains forbidden modifying keywords.")
return False
return True
except Exception as e:
print(f"[SQLService] Error during SQL validation: {e}")
return False
def _clean_query_tsql_artifacts(self, sql: str) -> str:
"""
Lightweight cleaner that removes accidental T-SQL syntax the LLM might
hallucinate when generating queries for a schema DB that was originally T-SQL.
Only applied to SELECT queries.
"""
cleaned = sql
# Convert SELECT TOP N ... to SELECT ... LIMIT N
top_match = re.search(
r'\bSELECT\s+TOP\s+(\d+)\b',
cleaned, re.IGNORECASE
)
if top_match:
limit_n = top_match.group(1)
# Remove TOP N from SELECT
cleaned = re.sub(r'\bSELECT\s+TOP\s+\d+\b', 'SELECT', cleaned, flags=re.IGNORECASE)
# Check for semicolon at the end
has_semicolon = cleaned.strip().endswith(';')
cleaned_body = cleaned.strip()
if has_semicolon:
cleaned_body = cleaned_body[:-1].strip()
# Remove any existing LIMIT to avoid duplication
cleaned_body = re.sub(r'\bLIMIT\s+\d+\s*$', '', cleaned_body, flags=re.IGNORECASE).strip()
# Append LIMIT at the end
cleaned = f"{cleaned_body} LIMIT {limit_n}"
if has_semicolon:
cleaned += ";"
# Remove WITH (NOLOCK) hints
cleaned = re.sub(r'\bWITH\s*\(\s*NOLOCK\s*\)', '', cleaned, flags=re.IGNORECASE)
# Remove square brackets around identifiers: [EMPNO] → EMPNO
cleaned = re.sub(r'\[([^\]]+)\]', r'\1', cleaned)
# Clean up any double spaces left behind
cleaned = re.sub(r' +', ' ', cleaned).strip()
return cleaned
def execute_query(self, db_name: str, sql: str, mode: str = "csv") -> Dict[str, Any]:
"""
Executes the provided SQL query against the specified SQLite database.
Validates the SQL first, limits output using pandas, measures execution time,
and translates SQLite/pandas dtypes to JSON-serializable Python objects.
"""
print(f"[SQLService] Executing SQL on {db_name}.db (mode={mode}): {sql}")
try:
# 1. Validate SQL safety
if not self.validate_sql(sql):
raise ValueError("Dangerous or invalid SQL query. Only SELECT queries are allowed.")
# 1b. Clean any accidental T-SQL artifacts from LLM-generated queries
if mode == "schema":
sql = self._clean_query_tsql_artifacts(sql)
print(f"[SQLService] After T-SQL cleanup: {sql}")
if mode == "csv":
db_path = os.path.join(config.DB_DIR, f"{db_name}.db")
else:
db_path = os.path.join(config.SCHEMA_DB_DIR, f"{db_name}.db")
if not os.path.exists(db_path):
raise FileNotFoundError(f"Database {db_name}.db does not exist.")
conn = sqlite3.connect(db_path)
# 2. Execute query and measure time
start_time = time.perf_counter()
try:
df = pd.read_sql_query(sql, conn)
except Exception as sql_err:
conn.close()
print(f"[SQLService] SQLite Error: {sql_err}")
raise ValueError(f"SQLite error: {str(sql_err)}")
execution_time_ms = (time.perf_counter() - start_time) * 1000.0
conn.close()
# 3. Limit rows to MAX_ROWS_RETURNED
df_limited = df.head(config.MAX_ROWS_RETURNED)
# 4. Extract columns and rows, converting numpy/pandas NaN and types to native types
columns = df_limited.columns.tolist()
rows = []
for row in df_limited.itertuples(index=False):
clean_row = []
for val in row:
if pd.isna(val):
clean_row.append(None)
elif hasattr(val, "item") and not isinstance(val, (str, bytes)):
# Convert numpy/pandas numeric scalars to Python scalars
clean_row.append(val.item())
else:
clean_row.append(val)
rows.append(clean_row)
result = {
"columns": columns,
"rows": rows,
"row_count": len(rows),
"execution_time_ms": round(execution_time_ms, 2)
}
print(f"[SQLService] Query execution succeeded. Returned {result['row_count']} rows in {result['execution_time_ms']} ms.")
return result
except Exception as e:
print(f"[SQLService] Error during SQL execution: {e}")
raise ValueError(str(e))
# Singleton instance
sql_service = SQLService()
|