Datavision / backend /mcp /sql_executor.py
DataVision CI/CD Bot
release: clean production build for HuggingFace Space
09801ca
Raw
History Blame Contribute Delete
11.8 kB
# MCP SQL Executor Module
"""
SQL execution tools for MCP integration using DuckDB.
Features:
- In-memory SQL execution
- DataFrame to table conversion
- Query result formatting
- Aggregation support
"""
from typing import Dict, List, Optional, Any
import json
# Global DuckDB connection
_connection = None
def _get_connection():
"""Get or create DuckDB connection"""
global _connection
if _connection is None:
try:
import duckdb
_connection = duckdb.connect(':memory:')
except ImportError:
return None
return _connection
def execute_sql(
query: str,
data: Optional[Dict[str, Any]] = None
) -> Dict:
"""
Execute SQL query on data.
Args:
query: SQL query string
data: Dict mapping table names to DataFrames
Returns:
Query results
"""
try:
conn = _get_connection()
if conn is None:
# Fallback to pandas-based execution
return _execute_sql_pandas(query, data)
import duckdb
# Register DataFrames as tables
if data:
for table_name, df_data in data.items():
import pandas as pd
if isinstance(df_data, pd.DataFrame):
df = df_data
elif isinstance(df_data, list):
df = pd.DataFrame(df_data)
elif isinstance(df_data, dict):
df = pd.DataFrame(df_data)
else:
continue
conn.register(table_name, df)
# Execute query
result = conn.execute(query)
# Get column names
columns = [desc[0] for desc in result.description] if result.description else []
# Fetch results
rows = result.fetchall()
# Convert to list of dicts
records = []
for row in rows:
record = {}
for i, col in enumerate(columns):
val = row[i]
# Convert to JSON-serializable types
if hasattr(val, 'item'): # numpy types
val = val.item()
record[col] = val
records.append(record)
return {
"success": True,
"columns": columns,
"data": records,
"row_count": len(records)
}
except Exception as e:
return {
"success": False,
"error": str(e),
"data": []
}
def _execute_sql_pandas(query: str, data: Optional[Dict] = None) -> Dict:
"""Fallback SQL execution using pandas and sqlite"""
try:
import pandas as pd
import sqlite3
# Create in-memory SQLite connection
conn = sqlite3.connect(':memory:')
# Load DataFrames as tables
if data:
for table_name, df_data in data.items():
if isinstance(df_data, pd.DataFrame):
df = df_data
elif isinstance(df_data, list):
df = pd.DataFrame(df_data)
elif isinstance(df_data, dict):
df = pd.DataFrame(df_data)
else:
continue
df.to_sql(table_name, conn, index=False, if_exists='replace')
# Execute query
result_df = pd.read_sql_query(query, conn)
conn.close()
return {
"success": True,
"columns": list(result_df.columns),
"data": result_df.to_dict(orient='records'),
"row_count": len(result_df)
}
except Exception as e:
return {
"success": False,
"error": str(e),
"data": []
}
def create_table_from_df(
name: str,
df: Any,
replace: bool = True
) -> Dict:
"""
Create virtual table from DataFrame.
Args:
name: Table name
df: DataFrame to register
replace: Whether to replace existing table
Returns:
Table creation result
"""
try:
conn = _get_connection()
if conn is None:
return {
"success": False,
"error": "DuckDB not available",
"table_name": None
}
import pandas as pd
if not isinstance(df, pd.DataFrame):
if isinstance(df, list):
df = pd.DataFrame(df)
elif isinstance(df, dict):
df = pd.DataFrame(df)
else:
return {
"success": False,
"error": "Invalid data type",
"table_name": None
}
conn.register(name, df)
return {
"success": True,
"table_name": name,
"columns": list(df.columns),
"row_count": len(df)
}
except Exception as e:
return {
"success": False,
"error": str(e),
"table_name": None
}
def query_to_dataframe(query: str) -> Dict:
"""
Execute query and return DataFrame result.
Args:
query: SQL query string
Returns:
Query result as DataFrame dict
"""
try:
conn = _get_connection()
if conn is None:
return {
"success": False,
"error": "DuckDB not available",
"dataframe": None
}
import pandas as pd
result_df = conn.execute(query).fetchdf()
return {
"success": True,
"dataframe": result_df.to_dict(orient='records'),
"columns": list(result_df.columns),
"row_count": len(result_df)
}
except Exception as e:
return {
"success": False,
"error": str(e),
"dataframe": None
}
def aggregate_data(
table_or_df: Any,
group_by: List[str],
aggregations: Dict[str, str]
) -> Dict:
"""
Perform aggregation on data.
Args:
table_or_df: Table name (str) or DataFrame
group_by: Columns to group by
aggregations: Dict of {column: agg_function}
agg_function: "sum", "avg", "count", "min", "max"
Returns:
Aggregated results
"""
try:
import pandas as pd
if isinstance(table_or_df, str):
# It's a table name, query it
conn = _get_connection()
if conn is None:
return {
"success": False,
"error": "DuckDB not available",
"data": None
}
df = conn.execute(f"SELECT * FROM {table_or_df}").fetchdf()
elif isinstance(table_or_df, pd.DataFrame):
df = table_or_df
else:
df = pd.DataFrame(table_or_df)
# Build aggregation dict for pandas
agg_map = {
"sum": "sum",
"avg": "mean",
"count": "count",
"min": "min",
"max": "max",
"mean": "mean"
}
pandas_agg = {}
for col, func in aggregations.items():
if col in df.columns:
pandas_agg[col] = agg_map.get(func.lower(), func)
# Perform aggregation
if group_by:
result = df.groupby(group_by).agg(pandas_agg).reset_index()
else:
result = df.agg(pandas_agg).to_frame().T
return {
"success": True,
"data": result.to_dict(orient='records'),
"columns": list(result.columns),
"row_count": len(result)
}
except Exception as e:
return {
"success": False,
"error": str(e),
"data": None
}
def join_tables(
left_table: Any,
right_table: Any,
left_on: str,
right_on: str,
how: str = "inner"
) -> Dict:
"""
Join two tables.
Args:
left_table: Left table name or DataFrame
right_table: Right table name or DataFrame
left_on: Left join column
right_on: Right join column
how: Join type ("inner", "left", "right", "outer")
Returns:
Joined table
"""
try:
import pandas as pd
# Convert to DataFrames
if isinstance(left_table, str):
conn = _get_connection()
left_df = conn.execute(f"SELECT * FROM {left_table}").fetchdf()
else:
left_df = pd.DataFrame(left_table)
if isinstance(right_table, str):
conn = _get_connection()
right_df = conn.execute(f"SELECT * FROM {right_table}").fetchdf()
else:
right_df = pd.DataFrame(right_table)
# Perform join
result = pd.merge(left_df, right_df, left_on=left_on, right_on=right_on, how=how)
return {
"success": True,
"data": result.to_dict(orient='records'),
"columns": list(result.columns),
"row_count": len(result)
}
except Exception as e:
return {
"success": False,
"error": str(e),
"data": None
}
def filter_data(
table_or_df: Any,
conditions: List[Dict]
) -> Dict:
"""
Filter data based on conditions.
Args:
table_or_df: Table name or DataFrame
conditions: List of {column, operator, value}
operator: "=", "!=", ">", "<", ">=", "<=", "in", "like"
Returns:
Filtered data
"""
try:
import pandas as pd
if isinstance(table_or_df, str):
conn = _get_connection()
df = conn.execute(f"SELECT * FROM {table_or_df}").fetchdf()
else:
df = pd.DataFrame(table_or_df)
result = df.copy()
for cond in conditions:
col = cond.get("column")
op = cond.get("operator", "=")
val = cond.get("value")
if col not in result.columns:
continue
if op == "=" or op == "==":
result = result[result[col] == val]
elif op == "!=":
result = result[result[col] != val]
elif op == ">":
result = result[result[col] > val]
elif op == "<":
result = result[result[col] < val]
elif op == ">=":
result = result[result[col] >= val]
elif op == "<=":
result = result[result[col] <= val]
elif op == "in":
result = result[result[col].isin(val)]
elif op == "like":
result = result[result[col].str.contains(val, case=False, na=False)]
return {
"success": True,
"data": result.to_dict(orient='records'),
"columns": list(result.columns),
"row_count": len(result),
"original_count": len(df)
}
except Exception as e:
return {
"success": False,
"error": str(e),
"data": None
}
def reset_connection():
"""Reset the DuckDB connection"""
global _connection
_connection = None
return {"success": True, "message": "Connection reset"}