text2sql-eval-toolkit 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- text2sql_eval_toolkit/__init__.py +107 -0
- text2sql_eval_toolkit/analysis/__init__.py +5 -0
- text2sql_eval_toolkit/analysis/error_analysis.py +335 -0
- text2sql_eval_toolkit/analysis/report_tools.py +719 -0
- text2sql_eval_toolkit/config_args.py +65 -0
- text2sql_eval_toolkit/data/__init__.py +4 -0
- text2sql_eval_toolkit/data/benchmarks.json +69 -0
- text2sql_eval_toolkit/data/test-benchmarks.json +69 -0
- text2sql_eval_toolkit/env_loader.py +55 -0
- text2sql_eval_toolkit/evaluation/__init__.py +26 -0
- text2sql_eval_toolkit/evaluation/evaluation_tools.py +759 -0
- text2sql_eval_toolkit/evaluation/llm_as_judge.py +90 -0
- text2sql_eval_toolkit/execution/__init__.py +5 -0
- text2sql_eval_toolkit/execution/execution_tools.py +1448 -0
- text2sql_eval_toolkit/execution/replace_select_tool.py +114 -0
- text2sql_eval_toolkit/inference/__init__.py +5 -0
- text2sql_eval_toolkit/inference/agentic_pipeline.py +2335 -0
- text2sql_eval_toolkit/inference/base_pipeline.py +11 -0
- text2sql_eval_toolkit/inference/baseline_llm_pipeline.py +372 -0
- text2sql_eval_toolkit/inference/inference_tools.py +769 -0
- text2sql_eval_toolkit/logging.py +54 -0
- text2sql_eval_toolkit/profiling/profiling_tools.py +185 -0
- text2sql_eval_toolkit/utils.py +302 -0
- text2sql_eval_toolkit-1.0.0.dist-info/METADATA +382 -0
- text2sql_eval_toolkit-1.0.0.dist-info/RECORD +28 -0
- text2sql_eval_toolkit-1.0.0.dist-info/WHEEL +5 -0
- text2sql_eval_toolkit-1.0.0.dist-info/licenses/LICENSE +201 -0
- text2sql_eval_toolkit-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1448 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright IBM Corp. 2025 - 2026
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
This module provides functions to execute SQL queries against a database and augment prediction data with actual query results.
|
|
8
|
+
|
|
9
|
+
Functions for postgres DBs:
|
|
10
|
+
1. `quote_mixed_case_columns(sql: str) -> str`:
|
|
11
|
+
- This function takes a SQL query string and ensures that any column names not in lowercase are enclosed in double quotes.
|
|
12
|
+
- This is necessary for PostgreSQL compatibility.
|
|
13
|
+
|
|
14
|
+
2. `run_sql_and_get_dataframe(connection_string: str, schema_name: str, sql: str) -> pd.DataFrame`:
|
|
15
|
+
- Executes the provided SQL query on the database using the given connection string and schema.
|
|
16
|
+
- Returns the query results as a pandas DataFrame.
|
|
17
|
+
|
|
18
|
+
3. `run_execution(benchmark_id: str)`:
|
|
19
|
+
- This function processes predictions for a specific benchmark, running each SQL query and augmenting the predictions with the actual dataframes.
|
|
20
|
+
- It retrieves the database engine information for the given benchmark_id, constructs the connection string, and loads prediction data from a JSON file.
|
|
21
|
+
- For each prediction, it attempts to execute the predicted SQL, catches exceptions, and stores either the resulting dataframe or an error message.
|
|
22
|
+
- Finally, it writes the updated predictions back to the JSON file.
|
|
23
|
+
|
|
24
|
+
Requirements:
|
|
25
|
+
- `os`
|
|
26
|
+
- `json`
|
|
27
|
+
- `pathlib`
|
|
28
|
+
- `pandas`
|
|
29
|
+
- `sqlalchemy`
|
|
30
|
+
- `sqlglot`
|
|
31
|
+
- `text2sql_eval_toolkit.utils` (assumed to provide `get_benchmark_info`)
|
|
32
|
+
|
|
33
|
+
Assumptions:
|
|
34
|
+
- The environment variable for the PostgreSQL connection string is set.
|
|
35
|
+
- The predictions file is in JSON format and contains a structure with model predictions, each having a 'predicted_sql' key.
|
|
36
|
+
- The database schema_name is correctly specified in the benchmark information.
|
|
37
|
+
- Supported database type is currently only PostgreSQL.
|
|
38
|
+
|
|
39
|
+
Assisted by watsonx Code Assistant
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
import asyncio
|
|
43
|
+
import asyncpg
|
|
44
|
+
import os
|
|
45
|
+
import importlib
|
|
46
|
+
import json
|
|
47
|
+
import pandas as pd
|
|
48
|
+
import re
|
|
49
|
+
import sqlite3
|
|
50
|
+
import time
|
|
51
|
+
from func_timeout import func_timeout, FunctionTimedOut
|
|
52
|
+
from io import StringIO
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
from sqlglot import parse_one, exp
|
|
55
|
+
from tqdm.asyncio import tqdm_asyncio
|
|
56
|
+
from text2sql_eval_toolkit.utils import (
|
|
57
|
+
get_benchmark_info,
|
|
58
|
+
get_gt_sqls,
|
|
59
|
+
parse_dataframe,
|
|
60
|
+
BENCHMARKS_FILE,
|
|
61
|
+
)
|
|
62
|
+
from text2sql_eval_toolkit.execution.replace_select_tool import (
|
|
63
|
+
replace_select_for_logic_ex,
|
|
64
|
+
)
|
|
65
|
+
from text2sql_eval_toolkit.logging import get_logger
|
|
66
|
+
from urllib.parse import urlparse, parse_qs, unquote
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
_ibm_db_mod = None
|
|
70
|
+
_sqlalchemy_mod = None
|
|
71
|
+
_aiomysql_mod = None
|
|
72
|
+
|
|
73
|
+
logger = get_logger(__name__)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _require_mysql_deps():
|
|
77
|
+
"""Import MySQL dependencies only when needed; raise a helpful error if missing."""
|
|
78
|
+
global _sqlalchemy_mod, _aiomysql_mod
|
|
79
|
+
|
|
80
|
+
if _sqlalchemy_mod is None:
|
|
81
|
+
try:
|
|
82
|
+
_sqlalchemy_mod = importlib.import_module("sqlalchemy.ext.asyncio")
|
|
83
|
+
except ModuleNotFoundError as e:
|
|
84
|
+
raise RuntimeError(
|
|
85
|
+
"MySQL support is optional and not installed. "
|
|
86
|
+
"Install extras with: pip install sqlalchemy[asyncio] asyncio-mysql (or aiomysql)"
|
|
87
|
+
) from e
|
|
88
|
+
|
|
89
|
+
# Also import the text function
|
|
90
|
+
try:
|
|
91
|
+
from sqlalchemy import text
|
|
92
|
+
|
|
93
|
+
return _sqlalchemy_mod, text
|
|
94
|
+
except ImportError as e:
|
|
95
|
+
raise RuntimeError(
|
|
96
|
+
"MySQL support is optional and not installed. "
|
|
97
|
+
"Install extras with: pip install sqlalchemy[asyncio] asyncio-mysql (or aiomysql)"
|
|
98
|
+
) from e
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def normalize_mysql_connection_string(
|
|
102
|
+
connection_string: str, db_id: str = None
|
|
103
|
+
) -> tuple[str, dict]:
|
|
104
|
+
"""
|
|
105
|
+
Normalize MySQL connection string and return connect_args.
|
|
106
|
+
Based on your working normalize_connection_string function.
|
|
107
|
+
"""
|
|
108
|
+
import re
|
|
109
|
+
from urllib.parse import urlparse, urlunparse
|
|
110
|
+
|
|
111
|
+
connect_args = {}
|
|
112
|
+
|
|
113
|
+
# Handle mysql:// with SSL parameters like your example
|
|
114
|
+
if connection_string.startswith("mysql://"):
|
|
115
|
+
# For async, we'll try aiomysql first, then fall back to asyncio-mysql
|
|
116
|
+
# But follow your SSL handling pattern
|
|
117
|
+
connection_string = connection_string.replace(
|
|
118
|
+
"mysql://", "mysql+aiomysql://", 1
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Parse the URL to modify the database part only if db_id is provided
|
|
122
|
+
if db_id:
|
|
123
|
+
parsed = urlparse(connection_string)
|
|
124
|
+
# Replace the database part (path) with the db_id
|
|
125
|
+
# Remove leading slash and any existing database name
|
|
126
|
+
new_path = f"/{db_id}"
|
|
127
|
+
# Rebuild the URL with the new database name
|
|
128
|
+
connection_string = urlunparse(
|
|
129
|
+
(
|
|
130
|
+
parsed.scheme,
|
|
131
|
+
parsed.netloc,
|
|
132
|
+
new_path,
|
|
133
|
+
parsed.params,
|
|
134
|
+
parsed.query,
|
|
135
|
+
parsed.fragment,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Handle SSL mode parameter conversion - remove problematic SSL params
|
|
140
|
+
if "sslMode=" in connection_string:
|
|
141
|
+
# Remove the sslMode parameter entirely and let driver handle SSL automatically
|
|
142
|
+
connection_string = re.sub(r"[&?]sslMode=[^&]*", "", connection_string)
|
|
143
|
+
|
|
144
|
+
# Determine if this is an IBM Cloud or other SSL-required connection
|
|
145
|
+
ssl_required = any(
|
|
146
|
+
domain in connection_string
|
|
147
|
+
for domain in ["databases.appdomain.cloud", "ssl=true", "sslMode="]
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if ssl_required:
|
|
151
|
+
# For aiomysql, let's try a simpler SSL approach
|
|
152
|
+
# Just enable SSL without complex verification for IBM Cloud
|
|
153
|
+
import ssl
|
|
154
|
+
|
|
155
|
+
ssl_context = ssl.create_default_context()
|
|
156
|
+
ssl_context.check_hostname = False
|
|
157
|
+
ssl_context.verify_mode = ssl.CERT_NONE
|
|
158
|
+
|
|
159
|
+
connect_args = {"ssl": ssl_context}
|
|
160
|
+
|
|
161
|
+
return connection_string, connect_args
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def quote_mysql_identifiers(sql: str) -> str:
|
|
165
|
+
"""
|
|
166
|
+
Quote MySQL identifiers using backticks instead of double quotes.
|
|
167
|
+
MySQL uses backticks (`) for identifier quoting, not double quotes.
|
|
168
|
+
"""
|
|
169
|
+
try:
|
|
170
|
+
tree = parse_one(sql)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.debug(f"Failed to parse SQL: {e}")
|
|
173
|
+
return sql
|
|
174
|
+
|
|
175
|
+
def quote_identifier_if_needed(identifier):
|
|
176
|
+
if isinstance(identifier, exp.Identifier):
|
|
177
|
+
name = identifier.name
|
|
178
|
+
# Check if identifier needs quoting (contains special chars, is reserved word, etc.)
|
|
179
|
+
if not identifier.args.get("quoted") and (
|
|
180
|
+
name != name.lower()
|
|
181
|
+
or any(char in name for char in [" ", "-", "."])
|
|
182
|
+
or name.upper()
|
|
183
|
+
in [
|
|
184
|
+
"ORDER",
|
|
185
|
+
"GROUP",
|
|
186
|
+
"SELECT",
|
|
187
|
+
"FROM",
|
|
188
|
+
"WHERE",
|
|
189
|
+
"JOIN",
|
|
190
|
+
] # Add more reserved words as needed
|
|
191
|
+
):
|
|
192
|
+
return exp.Identifier(this=name, quoted=True)
|
|
193
|
+
return identifier
|
|
194
|
+
elif isinstance(identifier, str):
|
|
195
|
+
if identifier != identifier.lower() or any(
|
|
196
|
+
char in identifier for char in [" ", "-", "."]
|
|
197
|
+
):
|
|
198
|
+
return exp.Identifier(this=identifier, quoted=True)
|
|
199
|
+
return exp.Identifier(this=identifier)
|
|
200
|
+
return identifier
|
|
201
|
+
|
|
202
|
+
for node in tree.walk():
|
|
203
|
+
if isinstance(node, exp.Column):
|
|
204
|
+
node.set("this", quote_identifier_if_needed(node.this))
|
|
205
|
+
if node.table:
|
|
206
|
+
node.set("table", quote_identifier_if_needed(node.table))
|
|
207
|
+
elif isinstance(node, exp.Alias):
|
|
208
|
+
alias = node.args.get("alias")
|
|
209
|
+
if isinstance(alias, exp.Identifier):
|
|
210
|
+
node.set("alias", quote_identifier_if_needed(alias))
|
|
211
|
+
|
|
212
|
+
# Use MySQL dialect to generate SQL with backticks
|
|
213
|
+
return tree.sql(dialect="mysql")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
async def run_sql_and_get_dataframe_mysql_async(
|
|
217
|
+
normalized_conn_str: str, connect_args: dict, db_id: str, sql: str, timeout: int = 90
|
|
218
|
+
) -> pd.DataFrame:
|
|
219
|
+
"""
|
|
220
|
+
Execute SQL query on MySQL using SQLAlchemy async engine with timeout.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
normalized_conn_str: MySQL connection string
|
|
224
|
+
connect_args: Connection arguments
|
|
225
|
+
db_id: Database ID to connect to
|
|
226
|
+
sql: SQL query to execute
|
|
227
|
+
timeout: Query timeout in seconds (default: 90)
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
pd.DataFrame: Query results
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
asyncio.TimeoutError: If query execution exceeds timeout
|
|
234
|
+
"""
|
|
235
|
+
sqlalchemy_async, text = _require_mysql_deps()
|
|
236
|
+
|
|
237
|
+
# If db_id is provided, create a new connection string for that specific database
|
|
238
|
+
if db_id:
|
|
239
|
+
final_conn_str, final_connect_args = normalize_mysql_connection_string(
|
|
240
|
+
normalized_conn_str.replace("mysql+aiomysql://", "mysql://"), db_id
|
|
241
|
+
)
|
|
242
|
+
else:
|
|
243
|
+
final_conn_str, final_connect_args = normalized_conn_str, connect_args
|
|
244
|
+
|
|
245
|
+
engine = sqlalchemy_async.create_async_engine(
|
|
246
|
+
final_conn_str,
|
|
247
|
+
pool_size=1,
|
|
248
|
+
max_overflow=0,
|
|
249
|
+
pool_pre_ping=True,
|
|
250
|
+
pool_recycle=3600,
|
|
251
|
+
echo=False,
|
|
252
|
+
connect_args=final_connect_args,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Log query execution start
|
|
256
|
+
sql_preview = sql[:100] + "..." if len(sql) > 100 else sql
|
|
257
|
+
logger.debug(f"Executing MySQL query on db_id={db_id}: {sql_preview}")
|
|
258
|
+
start_time = time.perf_counter()
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
# Wrap execution with timeout
|
|
262
|
+
async with asyncio.timeout(timeout):
|
|
263
|
+
async with engine.begin() as conn:
|
|
264
|
+
result = await conn.execute(text(sql))
|
|
265
|
+
if result.returns_rows:
|
|
266
|
+
rows = result.fetchall()
|
|
267
|
+
columns = list(result.keys())
|
|
268
|
+
data = [dict(zip(columns, row)) for row in rows]
|
|
269
|
+
else:
|
|
270
|
+
columns = []
|
|
271
|
+
data = []
|
|
272
|
+
|
|
273
|
+
elapsed = time.perf_counter() - start_time
|
|
274
|
+
logger.debug(f"Query completed in {elapsed:.2f}s, returned {len(data)} rows")
|
|
275
|
+
|
|
276
|
+
return await asyncio.to_thread(pd.DataFrame, data, columns=columns)
|
|
277
|
+
except asyncio.TimeoutError:
|
|
278
|
+
elapsed = time.perf_counter() - start_time
|
|
279
|
+
logger.error(f"Query timed out after {elapsed:.2f}s (limit: {timeout}s)")
|
|
280
|
+
raise
|
|
281
|
+
finally:
|
|
282
|
+
await engine.dispose()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
async def mysql_run_execution_async(
|
|
286
|
+
connection_string: str,
|
|
287
|
+
predictions_path: Path | str,
|
|
288
|
+
max_concurrent_tasks: int = 16,
|
|
289
|
+
per_query_timeout_s: int = 90,
|
|
290
|
+
force_rerun: bool = False
|
|
291
|
+
):
|
|
292
|
+
"""
|
|
293
|
+
Execute SQL queries against MySQL database asynchronously.
|
|
294
|
+
Each prediction record can specify its own db_id for database selection.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
connection_string: MySQL connection string (e.g., "mysql://user:pass@host:port/default_db")
|
|
298
|
+
predictions_path: Path to predictions JSON file
|
|
299
|
+
max_concurrent_tasks: Maximum number of concurrent database connections
|
|
300
|
+
per_query_timeout_s: Timeout for each SQL query in seconds (default: 90)
|
|
301
|
+
force_rerun: Force re-execution even if results exist
|
|
302
|
+
"""
|
|
303
|
+
sqlalchemy_async, text = _require_mysql_deps()
|
|
304
|
+
|
|
305
|
+
with open(predictions_path, "r") as pf:
|
|
306
|
+
predictions_data = json.load(pf)
|
|
307
|
+
|
|
308
|
+
logger.debug(f"Original MySQL connection string: {connection_string}")
|
|
309
|
+
|
|
310
|
+
# Test the base connection first
|
|
311
|
+
try:
|
|
312
|
+
normalized_conn_str, connect_args = normalize_mysql_connection_string(
|
|
313
|
+
connection_string
|
|
314
|
+
)
|
|
315
|
+
logger.debug(f"Normalized MySQL connection string: {normalized_conn_str}")
|
|
316
|
+
logger.debug(f"MySQL connect_args: {connect_args}")
|
|
317
|
+
|
|
318
|
+
# Create a test engine to verify base connection
|
|
319
|
+
test_engine = sqlalchemy_async.create_async_engine(
|
|
320
|
+
normalized_conn_str,
|
|
321
|
+
pool_size=1,
|
|
322
|
+
max_overflow=0,
|
|
323
|
+
pool_pre_ping=True,
|
|
324
|
+
pool_recycle=3600,
|
|
325
|
+
echo=False,
|
|
326
|
+
connect_args=connect_args,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Test the connection
|
|
330
|
+
logger.debug("Testing MySQL base connection...")
|
|
331
|
+
async with test_engine.begin() as conn:
|
|
332
|
+
result = await conn.execute(text("SELECT 1"))
|
|
333
|
+
test_result = result.fetchone()
|
|
334
|
+
logger.debug(f"MySQL base connection test successful: {test_result}")
|
|
335
|
+
|
|
336
|
+
await test_engine.dispose()
|
|
337
|
+
|
|
338
|
+
except Exception as e:
|
|
339
|
+
logger.error(f"Error connecting to MySQL database: {e}")
|
|
340
|
+
raise RuntimeError(f"Failed to connect to MySQL database: {e}") from e
|
|
341
|
+
|
|
342
|
+
semaphore = asyncio.Semaphore(max_concurrent_tasks)
|
|
343
|
+
|
|
344
|
+
# Counter and lock for tracking queries
|
|
345
|
+
query_count = 0
|
|
346
|
+
query_lock = asyncio.Lock()
|
|
347
|
+
|
|
348
|
+
async def run_sql_with_count(db_id: str, sql: str):
|
|
349
|
+
nonlocal query_count
|
|
350
|
+
df = await run_sql_and_get_dataframe_mysql_async(
|
|
351
|
+
normalized_conn_str, connect_args, db_id, sql, per_query_timeout_s
|
|
352
|
+
)
|
|
353
|
+
async with query_lock:
|
|
354
|
+
query_count += 1
|
|
355
|
+
return df
|
|
356
|
+
|
|
357
|
+
async def process_prediction(obj):
|
|
358
|
+
async with semaphore:
|
|
359
|
+
obj = json.loads(json.dumps(obj)) # deepcopy-safe
|
|
360
|
+
|
|
361
|
+
if "metadata" in obj and "sql" in obj["metadata"]:
|
|
362
|
+
obj["sql"] = obj["metadata"]["sql"]
|
|
363
|
+
|
|
364
|
+
# Get db_id from the prediction record, default to None (use connection string default)
|
|
365
|
+
record_db_id = obj.get("db_id")
|
|
366
|
+
|
|
367
|
+
gt_sqls = obj.get("sql")
|
|
368
|
+
if not isinstance(gt_sqls, list):
|
|
369
|
+
gt_sqls = [gt_sqls]
|
|
370
|
+
|
|
371
|
+
gt_dfs = []
|
|
372
|
+
for gt_sql in gt_sqls:
|
|
373
|
+
try:
|
|
374
|
+
gt_df = await run_sql_with_count(record_db_id, gt_sql)
|
|
375
|
+
gt_dfs.append(gt_df.to_json(orient="split"))
|
|
376
|
+
except asyncio.TimeoutError:
|
|
377
|
+
logger.error(f"GT SQL timed out after {per_query_timeout_s}s")
|
|
378
|
+
obj["gt_sql_execution_error"] = (
|
|
379
|
+
f"GT SQL timed out after {per_query_timeout_s}s"
|
|
380
|
+
)
|
|
381
|
+
# Continue to allow prediction execution even if GT times out
|
|
382
|
+
except Exception as e:
|
|
383
|
+
logger.error(f"Error running ground truth SQL: {e}")
|
|
384
|
+
logger.error(f"SQL: {gt_sql}")
|
|
385
|
+
raise e
|
|
386
|
+
|
|
387
|
+
obj["gt_df"] = gt_dfs if len(gt_dfs) > 1 else gt_dfs[0]
|
|
388
|
+
obj.pop("gt_sql_execution_error", None)
|
|
389
|
+
|
|
390
|
+
async def _run_and_store_df(
|
|
391
|
+
sql_key: str,
|
|
392
|
+
df_key: str,
|
|
393
|
+
error_key: str,
|
|
394
|
+
truncated_flag_key: str,
|
|
395
|
+
execution_time_key: str,
|
|
396
|
+
model_predictions: dict,
|
|
397
|
+
obj: dict,
|
|
398
|
+
run_sql_fn,
|
|
399
|
+
):
|
|
400
|
+
sql = model_predictions.get(sql_key)
|
|
401
|
+
if not sql or (df_key in model_predictions and not force_rerun):
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
# Use MySQL-specific identifier quoting
|
|
405
|
+
sql = quote_mysql_identifiers(sql)
|
|
406
|
+
model_predictions[sql_key] = sql
|
|
407
|
+
|
|
408
|
+
try:
|
|
409
|
+
# Time the execution
|
|
410
|
+
execution_start = time.perf_counter()
|
|
411
|
+
df = await run_sql_fn(record_db_id, sql)
|
|
412
|
+
execution_end = time.perf_counter()
|
|
413
|
+
execution_time_ms = (execution_end - execution_start) * 1000
|
|
414
|
+
|
|
415
|
+
# Determine max ground truth row count
|
|
416
|
+
if isinstance(obj["gt_df"], list):
|
|
417
|
+
max_gt_rows = max(
|
|
418
|
+
pd.read_json(gt, orient="split").shape[0]
|
|
419
|
+
for gt in obj["gt_df"]
|
|
420
|
+
)
|
|
421
|
+
else:
|
|
422
|
+
max_gt_rows = pd.read_json(obj["gt_df"], orient="split").shape[
|
|
423
|
+
0
|
|
424
|
+
]
|
|
425
|
+
|
|
426
|
+
# Truncate if too many rows
|
|
427
|
+
if df.shape[0] - max_gt_rows >= 10:
|
|
428
|
+
df = df.head(max_gt_rows + 9)
|
|
429
|
+
model_predictions[truncated_flag_key] = True
|
|
430
|
+
|
|
431
|
+
model_predictions[df_key] = df.to_json(orient="split")
|
|
432
|
+
model_predictions[execution_time_key] = round(execution_time_ms, 2)
|
|
433
|
+
model_predictions.pop(error_key, None)
|
|
434
|
+
|
|
435
|
+
except asyncio.TimeoutError:
|
|
436
|
+
model_predictions[error_key] = (
|
|
437
|
+
f"Error running SQL: timed out after {per_query_timeout_s}s"
|
|
438
|
+
)
|
|
439
|
+
model_predictions[execution_time_key] = None
|
|
440
|
+
logger.debug(f"SQL timed out after {per_query_timeout_s}s")
|
|
441
|
+
except Exception as e:
|
|
442
|
+
model_predictions[error_key] = f"Error running SQL: {e}"
|
|
443
|
+
model_predictions[execution_time_key] = None
|
|
444
|
+
logger.debug(f"SQL error: {e}")
|
|
445
|
+
|
|
446
|
+
for model_name, model_predictions in obj.get("predictions", {}).items():
|
|
447
|
+
await _run_and_store_df(
|
|
448
|
+
sql_key="predicted_sql",
|
|
449
|
+
df_key="predicted_df",
|
|
450
|
+
error_key="sql_execution_error",
|
|
451
|
+
truncated_flag_key="predicted_df_truncated",
|
|
452
|
+
execution_time_key="execution_time_ms",
|
|
453
|
+
model_predictions=model_predictions,
|
|
454
|
+
obj=obj,
|
|
455
|
+
run_sql_fn=run_sql_with_count,
|
|
456
|
+
)
|
|
457
|
+
await _run_and_store_df(
|
|
458
|
+
sql_key="logic_sql",
|
|
459
|
+
df_key="logic_df",
|
|
460
|
+
error_key="logic_sql_execution_error",
|
|
461
|
+
truncated_flag_key="logic_df_truncated",
|
|
462
|
+
execution_time_key="logic_execution_time_ms",
|
|
463
|
+
model_predictions=model_predictions,
|
|
464
|
+
obj=obj,
|
|
465
|
+
run_sql_fn=run_sql_with_count,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
return obj
|
|
469
|
+
|
|
470
|
+
tasks = [process_prediction(obj) for obj in predictions_data]
|
|
471
|
+
updated_predictions_data = await tqdm_asyncio.gather(
|
|
472
|
+
*tasks, desc="Executing MySQL SQL queries"
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
with open(predictions_path, "w") as pf:
|
|
476
|
+
json.dump(updated_predictions_data, pf, indent=4)
|
|
477
|
+
|
|
478
|
+
return query_count
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _require_ibm_db():
|
|
482
|
+
"""Import ibm_db only when needed; raise a helpful error if missing."""
|
|
483
|
+
global _ibm_db_mod
|
|
484
|
+
if _ibm_db_mod is None:
|
|
485
|
+
try:
|
|
486
|
+
_ibm_db_mod = importlib.import_module("ibm_db")
|
|
487
|
+
except ModuleNotFoundError as e:
|
|
488
|
+
raise RuntimeError(
|
|
489
|
+
"DB2 support is optional and not installed. "
|
|
490
|
+
"Install extras with: pip install .[db2] (or) pip install ibm-db>=3.2.6"
|
|
491
|
+
) from e
|
|
492
|
+
return _ibm_db_mod
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def quote_mixed_case_columns(sql: str) -> str:
|
|
496
|
+
try:
|
|
497
|
+
tree = parse_one(sql)
|
|
498
|
+
except Exception as e:
|
|
499
|
+
logger.debug(f"Failed to parse SQL: {e}")
|
|
500
|
+
return sql
|
|
501
|
+
|
|
502
|
+
def quote_identifier_if_needed(identifier):
|
|
503
|
+
if isinstance(identifier, exp.Identifier):
|
|
504
|
+
name = identifier.name
|
|
505
|
+
if not identifier.args.get("quoted") and name != name.lower():
|
|
506
|
+
return exp.Identifier(this=name, quoted=True)
|
|
507
|
+
return identifier
|
|
508
|
+
elif isinstance(identifier, str):
|
|
509
|
+
if identifier != identifier.lower():
|
|
510
|
+
return exp.Identifier(this=identifier, quoted=True)
|
|
511
|
+
return exp.Identifier(this=identifier)
|
|
512
|
+
return identifier
|
|
513
|
+
|
|
514
|
+
for node in tree.walk():
|
|
515
|
+
if isinstance(node, exp.Column):
|
|
516
|
+
node.set("this", quote_identifier_if_needed(node.this))
|
|
517
|
+
if node.table:
|
|
518
|
+
node.set("table", quote_identifier_if_needed(node.table))
|
|
519
|
+
elif isinstance(node, exp.Alias):
|
|
520
|
+
alias = node.args.get("alias")
|
|
521
|
+
if isinstance(alias, exp.Identifier):
|
|
522
|
+
node.set("alias", quote_identifier_if_needed(alias))
|
|
523
|
+
|
|
524
|
+
return tree.sql(dialect="postgres")
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
async def run_sql_and_get_dataframe_async(
|
|
528
|
+
pool, schema_name: str, sql: str, timeout: int = 90
|
|
529
|
+
) -> pd.DataFrame:
|
|
530
|
+
"""
|
|
531
|
+
Execute SQL query on PostgreSQL with timeout.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
pool: asyncpg connection pool
|
|
535
|
+
schema_name: PostgreSQL schema name
|
|
536
|
+
sql: SQL query to execute
|
|
537
|
+
timeout: Query timeout in seconds (default: 90)
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
pd.DataFrame: Query results as DataFrame
|
|
541
|
+
|
|
542
|
+
Raises:
|
|
543
|
+
asyncio.TimeoutError: If query execution exceeds timeout
|
|
544
|
+
"""
|
|
545
|
+
async def _execute_query():
|
|
546
|
+
async with pool.acquire() as conn:
|
|
547
|
+
# Set search_path for this connection
|
|
548
|
+
await conn.execute(f"SET search_path TO {schema_name}")
|
|
549
|
+
rows = await conn.fetch(sql)
|
|
550
|
+
if rows:
|
|
551
|
+
columns = rows[0].keys()
|
|
552
|
+
else:
|
|
553
|
+
columns = []
|
|
554
|
+
data = [dict(row) for row in rows]
|
|
555
|
+
# Use `to_thread` because DataFrame creation is not async
|
|
556
|
+
return await asyncio.to_thread(pd.DataFrame, data, columns=columns)
|
|
557
|
+
|
|
558
|
+
# Wrap execution with timeout
|
|
559
|
+
return await asyncio.wait_for(_execute_query(), timeout=timeout)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
async def postgres_run_execution_async(
|
|
563
|
+
connection_string, schema_name, predictions_path, max_concurrent_tasks: int = 16, per_query_timeout_s: int = 90, force_rerun: bool = False
|
|
564
|
+
):
|
|
565
|
+
"""
|
|
566
|
+
Execute SQL queries on PostgreSQL using asyncpg with timeout.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
connection_string: PostgreSQL connection string
|
|
570
|
+
schema_name: PostgreSQL schema name
|
|
571
|
+
predictions_path: Path to predictions JSON file
|
|
572
|
+
max_concurrent_tasks: Maximum number of concurrent database connections
|
|
573
|
+
per_query_timeout_s: Timeout for each SQL query in seconds (default: 90)
|
|
574
|
+
force_rerun: Force re-execution even if results exist
|
|
575
|
+
|
|
576
|
+
Returns:
|
|
577
|
+
int: Total number of queries executed
|
|
578
|
+
"""
|
|
579
|
+
with open(predictions_path, "r") as pf:
|
|
580
|
+
predictions_data = json.load(pf)
|
|
581
|
+
|
|
582
|
+
# Set search_path using server_settings parameter
|
|
583
|
+
pool = await asyncpg.create_pool(
|
|
584
|
+
dsn=connection_string,
|
|
585
|
+
min_size=1,
|
|
586
|
+
max_size=max_concurrent_tasks,
|
|
587
|
+
server_settings={'search_path': schema_name}
|
|
588
|
+
)
|
|
589
|
+
semaphore = asyncio.Semaphore(max_concurrent_tasks)
|
|
590
|
+
|
|
591
|
+
# Counter and lock
|
|
592
|
+
query_count = 0
|
|
593
|
+
query_lock = asyncio.Lock()
|
|
594
|
+
|
|
595
|
+
async def run_sql_with_count(sql):
|
|
596
|
+
nonlocal query_count
|
|
597
|
+
df = await run_sql_and_get_dataframe_async(pool, schema_name, sql, per_query_timeout_s)
|
|
598
|
+
async with query_lock:
|
|
599
|
+
query_count += 1
|
|
600
|
+
return df
|
|
601
|
+
|
|
602
|
+
async def process_prediction(obj):
|
|
603
|
+
async with semaphore:
|
|
604
|
+
obj = json.loads(json.dumps(obj)) # deepcopy-safe
|
|
605
|
+
|
|
606
|
+
if "metadata" in obj and "sql" in obj["metadata"]:
|
|
607
|
+
obj["sql"] = obj["metadata"]["sql"]
|
|
608
|
+
|
|
609
|
+
gt_sqls = obj.get("sql")
|
|
610
|
+
if not isinstance(gt_sqls, list):
|
|
611
|
+
gt_sqls = [gt_sqls]
|
|
612
|
+
|
|
613
|
+
gt_dfs = []
|
|
614
|
+
for gt_sql in gt_sqls:
|
|
615
|
+
try:
|
|
616
|
+
gt_df = await run_sql_with_count(gt_sql)
|
|
617
|
+
gt_dfs.append(gt_df.to_json(orient="split"))
|
|
618
|
+
except asyncio.TimeoutError:
|
|
619
|
+
logger.error(f"GT SQL timed out after {per_query_timeout_s}s")
|
|
620
|
+
obj["gt_sql_execution_error"] = (
|
|
621
|
+
f"GT SQL timed out after {per_query_timeout_s}s"
|
|
622
|
+
)
|
|
623
|
+
except Exception as e:
|
|
624
|
+
logger.error(f"Error running ground truth SQL: {e}")
|
|
625
|
+
logger.error(f"SQL: {gt_sql}")
|
|
626
|
+
obj["gt_sql_execution_error"] = (
|
|
627
|
+
f"Error running ground truth SQL: {e}"
|
|
628
|
+
)
|
|
629
|
+
# Continue processing other queries instead of raising
|
|
630
|
+
|
|
631
|
+
if gt_dfs:
|
|
632
|
+
obj["gt_df"] = gt_dfs if len(gt_dfs) > 1 else gt_dfs[0]
|
|
633
|
+
obj.pop("gt_sql_execution_error", None)
|
|
634
|
+
|
|
635
|
+
async def _run_and_store_df(
|
|
636
|
+
sql_key: str,
|
|
637
|
+
df_key: str,
|
|
638
|
+
error_key: str,
|
|
639
|
+
truncated_flag_key: str,
|
|
640
|
+
execution_time_key: str,
|
|
641
|
+
model_predictions: dict,
|
|
642
|
+
obj: dict,
|
|
643
|
+
run_sql_fn,
|
|
644
|
+
):
|
|
645
|
+
sql = model_predictions.get(sql_key)
|
|
646
|
+
if not sql or (df_key in model_predictions and not force_rerun):
|
|
647
|
+
return
|
|
648
|
+
|
|
649
|
+
sql = quote_mixed_case_columns(sql)
|
|
650
|
+
model_predictions[sql_key] = sql
|
|
651
|
+
|
|
652
|
+
try:
|
|
653
|
+
# Time the execution
|
|
654
|
+
execution_start = time.perf_counter()
|
|
655
|
+
df = await run_sql_fn(sql)
|
|
656
|
+
execution_end = time.perf_counter()
|
|
657
|
+
execution_time_ms = (execution_end - execution_start) * 1000
|
|
658
|
+
|
|
659
|
+
# Determine max ground truth row count
|
|
660
|
+
if isinstance(obj["gt_df"], list):
|
|
661
|
+
max_gt_rows = max(
|
|
662
|
+
pd.read_json(gt, orient="split").shape[0]
|
|
663
|
+
for gt in obj["gt_df"]
|
|
664
|
+
)
|
|
665
|
+
else:
|
|
666
|
+
max_gt_rows = pd.read_json(obj["gt_df"], orient="split").shape[
|
|
667
|
+
0
|
|
668
|
+
]
|
|
669
|
+
|
|
670
|
+
# Truncate if too many rows
|
|
671
|
+
if df.shape[0] - max_gt_rows >= 10:
|
|
672
|
+
df = df.head(max_gt_rows + 9)
|
|
673
|
+
model_predictions[truncated_flag_key] = True
|
|
674
|
+
|
|
675
|
+
model_predictions[df_key] = df.to_json(orient="split")
|
|
676
|
+
model_predictions[execution_time_key] = round(execution_time_ms, 2)
|
|
677
|
+
model_predictions.pop(error_key, None)
|
|
678
|
+
|
|
679
|
+
except asyncio.TimeoutError:
|
|
680
|
+
model_predictions[error_key] = (
|
|
681
|
+
f"Error running SQL: timed out after {per_query_timeout_s}s"
|
|
682
|
+
)
|
|
683
|
+
model_predictions[execution_time_key] = None
|
|
684
|
+
logger.debug(f"SQL timed out after {per_query_timeout_s}s")
|
|
685
|
+
except Exception as e:
|
|
686
|
+
model_predictions[error_key] = f"Error running SQL: {e}"
|
|
687
|
+
model_predictions[execution_time_key] = None
|
|
688
|
+
logger.debug(f"SQL error: {e}")
|
|
689
|
+
|
|
690
|
+
for model_name, model_predictions in obj.get("predictions", {}).items():
|
|
691
|
+
await _run_and_store_df(
|
|
692
|
+
sql_key="predicted_sql",
|
|
693
|
+
df_key="predicted_df",
|
|
694
|
+
error_key="sql_execution_error",
|
|
695
|
+
truncated_flag_key="predicted_df_truncated",
|
|
696
|
+
execution_time_key="execution_time_ms",
|
|
697
|
+
model_predictions=model_predictions,
|
|
698
|
+
obj=obj,
|
|
699
|
+
run_sql_fn=run_sql_with_count,
|
|
700
|
+
)
|
|
701
|
+
await _run_and_store_df(
|
|
702
|
+
sql_key="logic_sql",
|
|
703
|
+
df_key="logic_df",
|
|
704
|
+
error_key="logic_sql_execution_error",
|
|
705
|
+
truncated_flag_key="logic_df_truncated",
|
|
706
|
+
execution_time_key="logic_execution_time_ms",
|
|
707
|
+
model_predictions=model_predictions,
|
|
708
|
+
obj=obj,
|
|
709
|
+
run_sql_fn=run_sql_with_count,
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
return obj
|
|
713
|
+
|
|
714
|
+
tasks = [process_prediction(obj) for obj in predictions_data]
|
|
715
|
+
# updated_predictions_data = await asyncio.gather(*tasks)
|
|
716
|
+
updated_predictions_data = await tqdm_asyncio.gather(
|
|
717
|
+
*tasks, desc="Executing Postgres SQL queries"
|
|
718
|
+
)
|
|
719
|
+
await pool.close()
|
|
720
|
+
|
|
721
|
+
with open(predictions_path, "w") as pf:
|
|
722
|
+
json.dump(updated_predictions_data, pf, indent=4)
|
|
723
|
+
|
|
724
|
+
return query_count
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
def run_sqlite_query(db_path: str, sql: str) -> str:
|
|
728
|
+
conn = sqlite3.connect(db_path)
|
|
729
|
+
conn.text_factory = lambda b: b.decode(errors='replace')
|
|
730
|
+
conn.row_factory = sqlite3.Row
|
|
731
|
+
cursor = conn.execute(sql)
|
|
732
|
+
rows = cursor.fetchall()
|
|
733
|
+
columns = [col[0] for col in cursor.description]
|
|
734
|
+
data = [dict(zip(columns, row)) for row in rows]
|
|
735
|
+
conn.close()
|
|
736
|
+
return pd.DataFrame(data, columns=columns).to_json(orient="split")
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
async def run_sqlite_query_with_timeout(
|
|
740
|
+
db_path: Path, sql: str, timeout: int
|
|
741
|
+
) -> pd.DataFrame:
|
|
742
|
+
loop = asyncio.get_running_loop()
|
|
743
|
+
try:
|
|
744
|
+
json_result = await loop.run_in_executor(
|
|
745
|
+
None,
|
|
746
|
+
lambda: func_timeout(timeout, run_sqlite_query, args=(str(db_path), sql)),
|
|
747
|
+
)
|
|
748
|
+
return pd.read_json(StringIO(json_result), orient="split")
|
|
749
|
+
except FunctionTimedOut:
|
|
750
|
+
raise asyncio.TimeoutError(f"Query timed out after {timeout} seconds")
|
|
751
|
+
except Exception as e:
|
|
752
|
+
raise RuntimeError(f"Error running query: {e}")
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
async def sqlite_run_execution_async(
|
|
756
|
+
db_folder,
|
|
757
|
+
predictions_path,
|
|
758
|
+
max_concurrent_tasks: int = 32,
|
|
759
|
+
sql_execution_timeout: int = 5,
|
|
760
|
+
force_rerun: bool = False,
|
|
761
|
+
):
|
|
762
|
+
with open(predictions_path, "r") as pf:
|
|
763
|
+
predictions_data = json.load(pf)
|
|
764
|
+
|
|
765
|
+
semaphore = asyncio.Semaphore(max_concurrent_tasks)
|
|
766
|
+
query_count = 0
|
|
767
|
+
|
|
768
|
+
async def process_prediction(record):
|
|
769
|
+
nonlocal query_count
|
|
770
|
+
async with semaphore:
|
|
771
|
+
record = json.loads(json.dumps(record))
|
|
772
|
+
if "metadata" in record and "sql" in record["metadata"]:
|
|
773
|
+
record["sql"] = record["metadata"]["sql"]
|
|
774
|
+
db_id = record["db_id"]
|
|
775
|
+
db_filename = db_id + ".sqlite"
|
|
776
|
+
db_path = (
|
|
777
|
+
Path(BENCHMARKS_FILE).parent / Path(db_folder) / db_id / db_filename
|
|
778
|
+
)
|
|
779
|
+
if not db_path.exists():
|
|
780
|
+
raise ValueError(f"DB does not exist: {db_path}")
|
|
781
|
+
|
|
782
|
+
gt_sqls = get_gt_sqls(record)
|
|
783
|
+
|
|
784
|
+
gt_dfs = []
|
|
785
|
+
for gt_sql in gt_sqls:
|
|
786
|
+
try:
|
|
787
|
+
gt_df = await run_sqlite_query_with_timeout(
|
|
788
|
+
db_path, gt_sql, sql_execution_timeout
|
|
789
|
+
)
|
|
790
|
+
query_count += 1
|
|
791
|
+
gt_dfs.append(gt_df.to_json(orient="split"))
|
|
792
|
+
except asyncio.TimeoutError:
|
|
793
|
+
logger.error(f"Ground truth query timed out: {gt_sql}")
|
|
794
|
+
record["gt_sql_execution_error"] = "Ground truth query timed out."
|
|
795
|
+
except Exception as e:
|
|
796
|
+
logger.debug(f"DB path: {db_path}")
|
|
797
|
+
logger.error(f"Error running ground truth SQL: {e}")
|
|
798
|
+
logger.debug(f"Ground truth SQL with error: {gt_sql}")
|
|
799
|
+
record["gt_sql_execution_error"] = (
|
|
800
|
+
f"Error running ground truth SQL: {e}"
|
|
801
|
+
)
|
|
802
|
+
raise e
|
|
803
|
+
if len(gt_dfs) == 1:
|
|
804
|
+
record["gt_df"] = gt_dfs[0]
|
|
805
|
+
record.pop("gt_sql_execution_error", None)
|
|
806
|
+
elif len(gt_dfs) > 1:
|
|
807
|
+
record["gt_df"] = gt_dfs
|
|
808
|
+
record.pop("gt_sql_execution_error", None)
|
|
809
|
+
|
|
810
|
+
async def _run_sqlite_and_store(
|
|
811
|
+
sql_key: str,
|
|
812
|
+
df_key: str,
|
|
813
|
+
error_key: str,
|
|
814
|
+
truncated_flag_key: str,
|
|
815
|
+
execution_time_key: str,
|
|
816
|
+
model_predictions: dict,
|
|
817
|
+
record: dict,
|
|
818
|
+
db_path: str,
|
|
819
|
+
sql_execution_timeout: float,
|
|
820
|
+
query_count_ref: list,
|
|
821
|
+
):
|
|
822
|
+
sql = model_predictions.get(sql_key)
|
|
823
|
+
if not sql or (df_key in model_predictions and not force_rerun):
|
|
824
|
+
return
|
|
825
|
+
|
|
826
|
+
try:
|
|
827
|
+
# Time the execution
|
|
828
|
+
execution_start = time.perf_counter()
|
|
829
|
+
df = await run_sqlite_query_with_timeout(
|
|
830
|
+
db_path, sql, sql_execution_timeout
|
|
831
|
+
)
|
|
832
|
+
execution_end = time.perf_counter()
|
|
833
|
+
execution_time_ms = (execution_end - execution_start) * 1000
|
|
834
|
+
|
|
835
|
+
query_count_ref[0] += 1
|
|
836
|
+
logger.debug(f"Finished running SQL query #{query_count_ref[0]}.")
|
|
837
|
+
|
|
838
|
+
max_gt_rows = 10
|
|
839
|
+
if "gt_df" in record:
|
|
840
|
+
if isinstance(record["gt_df"], list):
|
|
841
|
+
max_gt_rows = max(
|
|
842
|
+
pd.read_json(StringIO(gt), orient="split").shape[0]
|
|
843
|
+
for gt in record["gt_df"]
|
|
844
|
+
)
|
|
845
|
+
else:
|
|
846
|
+
max_gt_rows = pd.read_json(
|
|
847
|
+
StringIO(record["gt_df"]), orient="split"
|
|
848
|
+
).shape[0]
|
|
849
|
+
|
|
850
|
+
if df.shape[0] - max_gt_rows >= 10:
|
|
851
|
+
df = df.head(max_gt_rows + 9)
|
|
852
|
+
model_predictions[truncated_flag_key] = True
|
|
853
|
+
|
|
854
|
+
model_predictions[df_key] = df.to_json(orient="split")
|
|
855
|
+
model_predictions[execution_time_key] = round(execution_time_ms, 2)
|
|
856
|
+
model_predictions.pop(error_key, None)
|
|
857
|
+
|
|
858
|
+
except asyncio.TimeoutError:
|
|
859
|
+
logger.info(f"{sql_key} query execution timed out: {sql}")
|
|
860
|
+
model_predictions[error_key] = (
|
|
861
|
+
f"{sql_key} query execution timed out: {sql}"
|
|
862
|
+
)
|
|
863
|
+
model_predictions[execution_time_key] = None
|
|
864
|
+
except Exception as e:
|
|
865
|
+
model_predictions[error_key] = f"Error running SQL: {e}"
|
|
866
|
+
model_predictions[execution_time_key] = None
|
|
867
|
+
logger.debug(f"{sql_key} error: {e}")
|
|
868
|
+
|
|
869
|
+
query_count_ref = [query_count] # wrap in list to mutate inside helper
|
|
870
|
+
for model_name, model_predictions in record.get("predictions", {}).items():
|
|
871
|
+
await _run_sqlite_and_store(
|
|
872
|
+
sql_key="predicted_sql",
|
|
873
|
+
df_key="predicted_df",
|
|
874
|
+
error_key="sql_execution_error",
|
|
875
|
+
truncated_flag_key="predicted_df_truncated",
|
|
876
|
+
execution_time_key="execution_time_ms",
|
|
877
|
+
model_predictions=model_predictions,
|
|
878
|
+
record=record,
|
|
879
|
+
db_path=db_path,
|
|
880
|
+
sql_execution_timeout=sql_execution_timeout,
|
|
881
|
+
query_count_ref=query_count_ref,
|
|
882
|
+
)
|
|
883
|
+
await _run_sqlite_and_store(
|
|
884
|
+
sql_key="logic_sql",
|
|
885
|
+
df_key="logic_df",
|
|
886
|
+
error_key="logic_sql_execution_error",
|
|
887
|
+
truncated_flag_key="logic_df_truncated",
|
|
888
|
+
execution_time_key="logic_execution_time_ms",
|
|
889
|
+
model_predictions=model_predictions,
|
|
890
|
+
record=record,
|
|
891
|
+
db_path=db_path,
|
|
892
|
+
sql_execution_timeout=sql_execution_timeout,
|
|
893
|
+
query_count_ref=query_count_ref,
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
return record
|
|
897
|
+
|
|
898
|
+
tasks = [process_prediction(obj) for obj in predictions_data]
|
|
899
|
+
# updated_predictions_data = await asyncio.gather(*tasks)
|
|
900
|
+
updated_predictions_data = await tqdm_asyncio.gather(
|
|
901
|
+
*tasks, desc="Executing SQLite SQL queries"
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
logger.debug("Writing updated predictions with execution dataframes to file...")
|
|
905
|
+
with open(predictions_path, "w") as pf:
|
|
906
|
+
json.dump(updated_predictions_data, pf, indent=4)
|
|
907
|
+
logger.debug("Finished writing.")
|
|
908
|
+
|
|
909
|
+
return query_count
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
_LIMIT_RE = re.compile(r"(?is)\s+LIMIT\s+(\d+)\s*$")
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def _parse_db2_dsn(dsn: str) -> dict:
|
|
917
|
+
parts = [p for p in dsn.strip().strip(";").split(";") if p.strip()]
|
|
918
|
+
kv = {}
|
|
919
|
+
for p in parts:
|
|
920
|
+
if "=" in p:
|
|
921
|
+
k, v = p.split("=", 1)
|
|
922
|
+
kv[k.strip().upper()] = v.strip()
|
|
923
|
+
return kv
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def _normalize_sql_for_db2(sql: str) -> str:
|
|
927
|
+
s = (sql or "").rstrip().rstrip(";")
|
|
928
|
+
m = _LIMIT_RE.search(s)
|
|
929
|
+
if m:
|
|
930
|
+
n = m.group(1)
|
|
931
|
+
s = _LIMIT_RE.sub(f" FETCH FIRST {n} ROWS ONLY", s)
|
|
932
|
+
return s
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
async def db2_run_execution_async(
|
|
936
|
+
connection_string: str,
|
|
937
|
+
schema_name: str | None,
|
|
938
|
+
predictions_path: Path | str,
|
|
939
|
+
max_concurrent_tasks: int = 16,
|
|
940
|
+
per_query_timeout_s: int = 90,
|
|
941
|
+
):
|
|
942
|
+
with open(predictions_path, "r") as pf:
|
|
943
|
+
predictions_data = json.load(pf)
|
|
944
|
+
|
|
945
|
+
dsn_kv = _parse_db2_dsn(connection_string)
|
|
946
|
+
dsn_schema = dsn_kv.get("CURRENTSCHEMA")
|
|
947
|
+
effective_schema = schema_name or dsn_schema
|
|
948
|
+
|
|
949
|
+
semaphore = asyncio.Semaphore(max_concurrent_tasks)
|
|
950
|
+
query_count = 0
|
|
951
|
+
query_lock = asyncio.Lock()
|
|
952
|
+
|
|
953
|
+
def _run_sql_and_get_dataframe_db2(sql: str) -> pd.DataFrame:
|
|
954
|
+
ibm_db = _require_ibm_db() # lazy import
|
|
955
|
+
fixed_sql = _normalize_sql_for_db2(sql)
|
|
956
|
+
|
|
957
|
+
conn = ibm_db.connect(connection_string, "", "")
|
|
958
|
+
try:
|
|
959
|
+
try:
|
|
960
|
+
ibm_db.autocommit(conn, ibm_db.SQL_AUTOCOMMIT_ON)
|
|
961
|
+
except Exception:
|
|
962
|
+
pass
|
|
963
|
+
|
|
964
|
+
if effective_schema:
|
|
965
|
+
ibm_db.exec_immediate(conn, f"SET CURRENT SCHEMA {effective_schema}")
|
|
966
|
+
|
|
967
|
+
stmt = ibm_db.prepare(conn, fixed_sql)
|
|
968
|
+
try:
|
|
969
|
+
ibm_db.set_option(
|
|
970
|
+
stmt, {ibm_db.SQL_ATTR_QUERY_TIMEOUT: per_query_timeout_s}, 0
|
|
971
|
+
)
|
|
972
|
+
except Exception:
|
|
973
|
+
pass
|
|
974
|
+
|
|
975
|
+
ok = ibm_db.execute(stmt)
|
|
976
|
+
rows, cols = [], []
|
|
977
|
+
if ok and ibm_db.num_fields(stmt) > 0:
|
|
978
|
+
ncols = ibm_db.num_fields(stmt)
|
|
979
|
+
cols = [ibm_db.field_name(stmt, i) for i in range(ncols)]
|
|
980
|
+
tup = ibm_db.fetch_tuple(stmt)
|
|
981
|
+
while tup:
|
|
982
|
+
rows.append(tup)
|
|
983
|
+
tup = ibm_db.fetch_tuple(stmt)
|
|
984
|
+
|
|
985
|
+
ibm_db.free_stmt(stmt)
|
|
986
|
+
return pd.DataFrame(rows, columns=cols)
|
|
987
|
+
finally:
|
|
988
|
+
ibm_db.close(conn)
|
|
989
|
+
|
|
990
|
+
async def run_sql_and_get_dataframe_async_db2(sql: str) -> pd.DataFrame:
|
|
991
|
+
return await asyncio.wait_for(
|
|
992
|
+
asyncio.to_thread(_run_sql_and_get_dataframe_db2, sql),
|
|
993
|
+
timeout=per_query_timeout_s + 5,
|
|
994
|
+
)
|
|
995
|
+
|
|
996
|
+
async def run_sql_with_count(sql: str) -> pd.DataFrame:
|
|
997
|
+
nonlocal query_count
|
|
998
|
+
df = await run_sql_and_get_dataframe_async_db2(sql)
|
|
999
|
+
async with query_lock:
|
|
1000
|
+
query_count += 1
|
|
1001
|
+
return df
|
|
1002
|
+
|
|
1003
|
+
async def process_prediction(obj: dict):
|
|
1004
|
+
async with semaphore:
|
|
1005
|
+
obj = json.loads(json.dumps(obj)) # deepcopy-safe
|
|
1006
|
+
|
|
1007
|
+
if "metadata" in obj and "sql" in obj["metadata"]:
|
|
1008
|
+
obj["sql"] = obj["metadata"]["sql"]
|
|
1009
|
+
|
|
1010
|
+
gt_sqls = obj.get("sql")
|
|
1011
|
+
if not isinstance(gt_sqls, list):
|
|
1012
|
+
gt_sqls = [gt_sqls]
|
|
1013
|
+
|
|
1014
|
+
gt_dfs = []
|
|
1015
|
+
for gt_sql in gt_sqls:
|
|
1016
|
+
try:
|
|
1017
|
+
gt_df = await run_sql_with_count(gt_sql)
|
|
1018
|
+
gt_dfs.append(gt_df.to_json(orient="split"))
|
|
1019
|
+
except asyncio.TimeoutError:
|
|
1020
|
+
logger.error(f"GT SQL timed out after {per_query_timeout_s}s")
|
|
1021
|
+
obj["gt_sql_execution_error"] = (
|
|
1022
|
+
f"GT SQL timed out after {per_query_timeout_s}s"
|
|
1023
|
+
)
|
|
1024
|
+
# raise
|
|
1025
|
+
except Exception as e:
|
|
1026
|
+
logger.error(f"Error running ground truth SQL: {e}")
|
|
1027
|
+
logger.error(f"SQL: {gt_sql}")
|
|
1028
|
+
obj["gt_sql_execution_error"] = (
|
|
1029
|
+
f"Error running ground truth SQL: {e}"
|
|
1030
|
+
)
|
|
1031
|
+
# raise
|
|
1032
|
+
if len(gt_dfs) > 0:
|
|
1033
|
+
obj["gt_df"] = gt_dfs if len(gt_dfs) > 1 else gt_dfs[0]
|
|
1034
|
+
obj.pop("gt_sql_execution_error", None)
|
|
1035
|
+
|
|
1036
|
+
async def _run_and_store_df(
|
|
1037
|
+
sql_key: str,
|
|
1038
|
+
df_key: str,
|
|
1039
|
+
error_key: str,
|
|
1040
|
+
truncated_flag_key: str,
|
|
1041
|
+
execution_time_key: str,
|
|
1042
|
+
model_predictions: dict,
|
|
1043
|
+
obj: dict,
|
|
1044
|
+
run_sql_fn,
|
|
1045
|
+
):
|
|
1046
|
+
sql = model_predictions.get(sql_key)
|
|
1047
|
+
if not sql or df_key in model_predictions:
|
|
1048
|
+
return
|
|
1049
|
+
|
|
1050
|
+
sql = quote_mixed_case_columns(sql)
|
|
1051
|
+
model_predictions[sql_key] = sql
|
|
1052
|
+
|
|
1053
|
+
try:
|
|
1054
|
+
# Time the execution
|
|
1055
|
+
execution_start = time.perf_counter()
|
|
1056
|
+
df = await run_sql_fn(sql)
|
|
1057
|
+
execution_end = time.perf_counter()
|
|
1058
|
+
execution_time_ms = (execution_end - execution_start) * 1000
|
|
1059
|
+
|
|
1060
|
+
if isinstance(obj["gt_df"], list):
|
|
1061
|
+
max_gt_rows = max(
|
|
1062
|
+
pd.read_json(gt, orient="split").shape[0]
|
|
1063
|
+
for gt in obj["gt_df"]
|
|
1064
|
+
)
|
|
1065
|
+
else:
|
|
1066
|
+
max_gt_rows = pd.read_json(obj["gt_df"], orient="split").shape[
|
|
1067
|
+
0
|
|
1068
|
+
]
|
|
1069
|
+
|
|
1070
|
+
if df.shape[0] - max_gt_rows >= 10:
|
|
1071
|
+
df = df.head(max_gt_rows + 9)
|
|
1072
|
+
model_predictions[truncated_flag_key] = True
|
|
1073
|
+
|
|
1074
|
+
model_predictions[df_key] = df.to_json(orient="split")
|
|
1075
|
+
model_predictions[execution_time_key] = round(execution_time_ms, 2)
|
|
1076
|
+
model_predictions.pop(error_key, None)
|
|
1077
|
+
|
|
1078
|
+
except asyncio.TimeoutError:
|
|
1079
|
+
model_predictions[error_key] = (
|
|
1080
|
+
f"Error running SQL: timed out after {per_query_timeout_s}s"
|
|
1081
|
+
)
|
|
1082
|
+
model_predictions[execution_time_key] = None
|
|
1083
|
+
except Exception as e:
|
|
1084
|
+
model_predictions[error_key] = f"Error running SQL: {e}"
|
|
1085
|
+
model_predictions[execution_time_key] = None
|
|
1086
|
+
logger.debug(f"SQL error: {e}")
|
|
1087
|
+
|
|
1088
|
+
for _, model_predictions in obj.get("predictions", {}).items():
|
|
1089
|
+
await _run_and_store_df(
|
|
1090
|
+
sql_key="predicted_sql",
|
|
1091
|
+
df_key="predicted_df",
|
|
1092
|
+
error_key="sql_execution_error",
|
|
1093
|
+
truncated_flag_key="predicted_df_truncated",
|
|
1094
|
+
execution_time_key="execution_time_ms",
|
|
1095
|
+
model_predictions=model_predictions,
|
|
1096
|
+
obj=obj,
|
|
1097
|
+
run_sql_fn=run_sql_with_count,
|
|
1098
|
+
)
|
|
1099
|
+
await _run_and_store_df(
|
|
1100
|
+
sql_key="logic_sql",
|
|
1101
|
+
df_key="logic_df",
|
|
1102
|
+
error_key="logic_sql_execution_error",
|
|
1103
|
+
truncated_flag_key="logic_df_truncated",
|
|
1104
|
+
execution_time_key="logic_execution_time_ms",
|
|
1105
|
+
model_predictions=model_predictions,
|
|
1106
|
+
obj=obj,
|
|
1107
|
+
run_sql_fn=run_sql_with_count,
|
|
1108
|
+
)
|
|
1109
|
+
|
|
1110
|
+
return obj
|
|
1111
|
+
|
|
1112
|
+
tasks = [process_prediction(obj) for obj in predictions_data]
|
|
1113
|
+
updated_predictions_data = await tqdm_asyncio.gather(
|
|
1114
|
+
*tasks, desc="Executing DB2 SQL queries"
|
|
1115
|
+
)
|
|
1116
|
+
with open(predictions_path, "w") as pf:
|
|
1117
|
+
json.dump(updated_predictions_data, pf, indent=4)
|
|
1118
|
+
return query_count
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def _parse_presto_sqlalchemy_url(conn_str: str) -> dict:
|
|
1122
|
+
"""
|
|
1123
|
+
Parse a SQLAlchemy-style Presto URL into prestodb.dbapi.connect(**kwargs) args.
|
|
1124
|
+
|
|
1125
|
+
Example input:
|
|
1126
|
+
presto://user:pass@host:30624/catalog/schema?currentSchema=schema
|
|
1127
|
+
"""
|
|
1128
|
+
u = urlparse(conn_str)
|
|
1129
|
+
username = unquote(u.username or "")
|
|
1130
|
+
password = unquote(u.password or "")
|
|
1131
|
+
host = u.hostname
|
|
1132
|
+
port = u.port or 443 # default to 443 for https
|
|
1133
|
+
# path like "/catalog/schema"
|
|
1134
|
+
path_parts = [p for p in (u.path or "").split("/") if p]
|
|
1135
|
+
catalog = path_parts[0] if len(path_parts) >= 1 else None
|
|
1136
|
+
schema = path_parts[1] if len(path_parts) >= 2 else None
|
|
1137
|
+
|
|
1138
|
+
qs = parse_qs(u.query or "")
|
|
1139
|
+
# Allow overriding schema via ?currentSchema=...
|
|
1140
|
+
if "currentSchema" in qs and qs["currentSchema"]:
|
|
1141
|
+
schema = qs["currentSchema"][0]
|
|
1142
|
+
|
|
1143
|
+
# Build BasicAuthentication if password is provided
|
|
1144
|
+
import prestodb # local import to keep optional
|
|
1145
|
+
|
|
1146
|
+
auth = prestodb.auth.BasicAuthentication(username, password) if password else None
|
|
1147
|
+
|
|
1148
|
+
# Filter out Nones
|
|
1149
|
+
kwargs = {
|
|
1150
|
+
"host": host,
|
|
1151
|
+
"port": port,
|
|
1152
|
+
"user": username,
|
|
1153
|
+
"catalog": catalog,
|
|
1154
|
+
"schema": schema,
|
|
1155
|
+
"http_scheme": "https",
|
|
1156
|
+
"auth": auth,
|
|
1157
|
+
"source": "text2sql-eval",
|
|
1158
|
+
}
|
|
1159
|
+
return {k: v for k, v in kwargs.items() if v is not None}
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
async def presto_run_execution_async(
|
|
1163
|
+
connection_string: str,
|
|
1164
|
+
predictions_path: Path | str,
|
|
1165
|
+
max_concurrent_tasks: int = 16,
|
|
1166
|
+
per_query_timeout_s: int = 300,
|
|
1167
|
+
):
|
|
1168
|
+
"""
|
|
1169
|
+
Execute SQL queries on Presto (IBM Lakehouse) using prestodb.dbapi with BasicAuthentication.
|
|
1170
|
+
- connection_string: SQLAlchemy-style Presto URL
|
|
1171
|
+
- predictions_path: JSON file with records and model predictions
|
|
1172
|
+
- max_concurrent_tasks: concurrency bound
|
|
1173
|
+
- per_query_timeout_s: per-query timeout (client-side)
|
|
1174
|
+
"""
|
|
1175
|
+
import prestodb
|
|
1176
|
+
import pandas as pd
|
|
1177
|
+
import asyncio
|
|
1178
|
+
import json
|
|
1179
|
+
|
|
1180
|
+
# Parse once and reuse for all queries
|
|
1181
|
+
base_connect_kwargs = _parse_presto_sqlalchemy_url(connection_string)
|
|
1182
|
+
logger.debug(
|
|
1183
|
+
f"Presto connect kwargs (redacted): "
|
|
1184
|
+
f"{ {k: ('***' if k == 'auth' else v) for k, v in base_connect_kwargs.items()} }"
|
|
1185
|
+
)
|
|
1186
|
+
|
|
1187
|
+
# Simple connectivity check
|
|
1188
|
+
try:
|
|
1189
|
+
|
|
1190
|
+
def _ping():
|
|
1191
|
+
conn = prestodb.dbapi.connect(**base_connect_kwargs)
|
|
1192
|
+
cur = conn.cursor()
|
|
1193
|
+
cur.execute("SELECT 1")
|
|
1194
|
+
cur.fetchall()
|
|
1195
|
+
cur.close()
|
|
1196
|
+
conn.close()
|
|
1197
|
+
|
|
1198
|
+
await asyncio.wait_for(asyncio.to_thread(_ping), timeout=30)
|
|
1199
|
+
logger.debug("Presto connectivity check succeeded.")
|
|
1200
|
+
except Exception as e:
|
|
1201
|
+
logger.error(f"Failed Presto connectivity check: {e}")
|
|
1202
|
+
raise RuntimeError(f"Failed to connect to Presto: {e}") from e
|
|
1203
|
+
|
|
1204
|
+
# Helper: run one SQL and return DataFrame (threaded, with timeout)
|
|
1205
|
+
def _run_presto_sql_to_df(sql: str) -> pd.DataFrame:
|
|
1206
|
+
conn = prestodb.dbapi.connect(**base_connect_kwargs)
|
|
1207
|
+
try:
|
|
1208
|
+
cur = conn.cursor()
|
|
1209
|
+
cur.execute(sql)
|
|
1210
|
+
rows = cur.fetchall() or []
|
|
1211
|
+
cols = [d[0] for d in (cur.description or [])]
|
|
1212
|
+
cur.close()
|
|
1213
|
+
return pd.DataFrame(rows, columns=cols)
|
|
1214
|
+
finally:
|
|
1215
|
+
conn.close()
|
|
1216
|
+
|
|
1217
|
+
async def run_sql_and_get_dataframe_presto(sql: str) -> pd.DataFrame:
|
|
1218
|
+
# client-side timeout guard; Presto may continue server-side even if we time out here
|
|
1219
|
+
return await asyncio.wait_for(
|
|
1220
|
+
asyncio.to_thread(_run_presto_sql_to_df, sql),
|
|
1221
|
+
timeout=per_query_timeout_s,
|
|
1222
|
+
)
|
|
1223
|
+
|
|
1224
|
+
# Orchestrate over predictions
|
|
1225
|
+
with open(predictions_path, "r") as pf:
|
|
1226
|
+
predictions_data = json.load(pf)
|
|
1227
|
+
|
|
1228
|
+
semaphore = asyncio.Semaphore(max_concurrent_tasks)
|
|
1229
|
+
|
|
1230
|
+
# Shared counter + lock
|
|
1231
|
+
query_count = 0
|
|
1232
|
+
query_lock = asyncio.Lock()
|
|
1233
|
+
|
|
1234
|
+
async def run_sql_with_count(sql: str) -> pd.DataFrame:
|
|
1235
|
+
nonlocal query_count
|
|
1236
|
+
df = await run_sql_and_get_dataframe_presto(sql)
|
|
1237
|
+
async with query_lock:
|
|
1238
|
+
query_count += 1
|
|
1239
|
+
return df
|
|
1240
|
+
|
|
1241
|
+
async def process_record(obj: dict):
|
|
1242
|
+
async with semaphore:
|
|
1243
|
+
obj = json.loads(json.dumps(obj)) # deep-copy-safe
|
|
1244
|
+
|
|
1245
|
+
# Normalize where GT SQL lives
|
|
1246
|
+
if "metadata" in obj and "sql" in obj["metadata"]:
|
|
1247
|
+
obj["sql"] = obj["metadata"]["sql"]
|
|
1248
|
+
|
|
1249
|
+
# Get GT sql(s)
|
|
1250
|
+
gt_sqls = get_gt_sqls(obj) if callable(get_gt_sqls) else obj.get("sql")
|
|
1251
|
+
if not isinstance(gt_sqls, list):
|
|
1252
|
+
gt_sqls = [gt_sqls]
|
|
1253
|
+
|
|
1254
|
+
# Ground truth execution
|
|
1255
|
+
gt_dfs = []
|
|
1256
|
+
for gt_sql in gt_sqls:
|
|
1257
|
+
if not gt_sql:
|
|
1258
|
+
continue
|
|
1259
|
+
try:
|
|
1260
|
+
df = await run_sql_with_count(gt_sql)
|
|
1261
|
+
gt_dfs.append(df.to_json(orient="split"))
|
|
1262
|
+
except asyncio.TimeoutError:
|
|
1263
|
+
logger.error(f"GT SQL timed out after {per_query_timeout_s}s")
|
|
1264
|
+
obj["gt_sql_execution_error"] = (
|
|
1265
|
+
f"GT SQL timed out after {per_query_timeout_s}s"
|
|
1266
|
+
)
|
|
1267
|
+
except Exception as e:
|
|
1268
|
+
logger.error(f"Error running GT SQL: {e}")
|
|
1269
|
+
logger.debug(f"GT SQL was: {gt_sql}")
|
|
1270
|
+
obj["gt_sql_execution_error"] = (
|
|
1271
|
+
f"Error running ground truth SQL: {e}"
|
|
1272
|
+
)
|
|
1273
|
+
# Keep going, or:
|
|
1274
|
+
# raise e
|
|
1275
|
+
|
|
1276
|
+
if gt_dfs:
|
|
1277
|
+
obj["gt_df"] = gt_dfs if len(gt_dfs) > 1 else gt_dfs[0]
|
|
1278
|
+
obj.pop("gt_sql_execution_error", None)
|
|
1279
|
+
|
|
1280
|
+
# Helper for model SQLs
|
|
1281
|
+
async def _run_and_store_df(
|
|
1282
|
+
sql_key: str,
|
|
1283
|
+
df_key: str,
|
|
1284
|
+
error_key: str,
|
|
1285
|
+
truncated_flag_key: str,
|
|
1286
|
+
execution_time_key: str,
|
|
1287
|
+
model_predictions: dict,
|
|
1288
|
+
obj_ref: dict,
|
|
1289
|
+
):
|
|
1290
|
+
sql = model_predictions.get(sql_key)
|
|
1291
|
+
if not sql or df_key in model_predictions:
|
|
1292
|
+
return
|
|
1293
|
+
|
|
1294
|
+
# Quote mixed-case identifiers for Presto (double-quotes)
|
|
1295
|
+
sql = quote_mixed_case_columns(sql)
|
|
1296
|
+
model_predictions[sql_key] = sql
|
|
1297
|
+
|
|
1298
|
+
try:
|
|
1299
|
+
# Time the execution
|
|
1300
|
+
execution_start = time.perf_counter()
|
|
1301
|
+
df = await run_sql_with_count(sql)
|
|
1302
|
+
execution_end = time.perf_counter()
|
|
1303
|
+
execution_time_ms = (execution_end - execution_start) * 1000
|
|
1304
|
+
|
|
1305
|
+
# Establish reference size from GT (max of multiple GTs if present)
|
|
1306
|
+
max_gt_rows = 0
|
|
1307
|
+
if "gt_df" in obj_ref and obj_ref["gt_df"]:
|
|
1308
|
+
if isinstance(obj_ref["gt_df"], list):
|
|
1309
|
+
max_gt_rows = max(
|
|
1310
|
+
pd.read_json(gt_json, orient="split").shape[0]
|
|
1311
|
+
for gt_json in obj_ref["gt_df"]
|
|
1312
|
+
)
|
|
1313
|
+
else:
|
|
1314
|
+
max_gt_rows = pd.read_json(
|
|
1315
|
+
obj_ref["gt_df"], orient="split"
|
|
1316
|
+
).shape[0]
|
|
1317
|
+
|
|
1318
|
+
# Truncate if too many extra rows
|
|
1319
|
+
if max_gt_rows and (df.shape[0] - max_gt_rows >= 10):
|
|
1320
|
+
df = df.head(max_gt_rows + 9)
|
|
1321
|
+
model_predictions[truncated_flag_key] = True
|
|
1322
|
+
|
|
1323
|
+
model_predictions[df_key] = df.to_json(orient="split")
|
|
1324
|
+
model_predictions[execution_time_key] = round(execution_time_ms, 2)
|
|
1325
|
+
model_predictions.pop(error_key, None)
|
|
1326
|
+
|
|
1327
|
+
except asyncio.TimeoutError:
|
|
1328
|
+
model_predictions[error_key] = (
|
|
1329
|
+
f"Error running SQL: timed out after {per_query_timeout_s}s"
|
|
1330
|
+
)
|
|
1331
|
+
model_predictions[execution_time_key] = None
|
|
1332
|
+
except Exception as e:
|
|
1333
|
+
model_predictions[error_key] = f"Error running SQL: {e}"
|
|
1334
|
+
model_predictions[execution_time_key] = None
|
|
1335
|
+
logger.debug(f"{sql_key} error: {e}")
|
|
1336
|
+
|
|
1337
|
+
# Execute model predictions
|
|
1338
|
+
for _, model_predictions in obj.get("predictions", {}).items():
|
|
1339
|
+
await _run_and_store_df(
|
|
1340
|
+
sql_key="predicted_sql",
|
|
1341
|
+
df_key="predicted_df",
|
|
1342
|
+
error_key="sql_execution_error",
|
|
1343
|
+
truncated_flag_key="predicted_df_truncated",
|
|
1344
|
+
execution_time_key="execution_time_ms",
|
|
1345
|
+
model_predictions=model_predictions,
|
|
1346
|
+
obj_ref=obj,
|
|
1347
|
+
)
|
|
1348
|
+
await _run_and_store_df(
|
|
1349
|
+
sql_key="logic_sql",
|
|
1350
|
+
df_key="logic_df",
|
|
1351
|
+
error_key="logic_sql_execution_error",
|
|
1352
|
+
truncated_flag_key="logic_df_truncated",
|
|
1353
|
+
execution_time_key="logic_execution_time_ms",
|
|
1354
|
+
model_predictions=model_predictions,
|
|
1355
|
+
obj_ref=obj,
|
|
1356
|
+
)
|
|
1357
|
+
|
|
1358
|
+
return obj
|
|
1359
|
+
|
|
1360
|
+
tasks = [process_record(obj) for obj in predictions_data]
|
|
1361
|
+
updated_predictions_data = await tqdm_asyncio.gather(
|
|
1362
|
+
*tasks, desc="Executing Presto SQL queries"
|
|
1363
|
+
)
|
|
1364
|
+
|
|
1365
|
+
with open(predictions_path, "w") as pf:
|
|
1366
|
+
json.dump(updated_predictions_data, pf, indent=4)
|
|
1367
|
+
|
|
1368
|
+
return query_count
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
# For running from script
|
|
1372
|
+
def run_execution(benchmark_id: str, num_threads: int = 16, force_rerun: bool = False):
|
|
1373
|
+
benchmark_info = get_benchmark_info(benchmark_id)
|
|
1374
|
+
predictions_path = Path(benchmark_info["predictions_path"])
|
|
1375
|
+
db_engine = benchmark_info["db_engine"]
|
|
1376
|
+
|
|
1377
|
+
replace_select_for_logic_ex(predictions_path, db_engine)
|
|
1378
|
+
|
|
1379
|
+
if db_engine["db_type"] not in [
|
|
1380
|
+
"postgres",
|
|
1381
|
+
"sqlite",
|
|
1382
|
+
"db2",
|
|
1383
|
+
"mysql",
|
|
1384
|
+
"presto",
|
|
1385
|
+
]:
|
|
1386
|
+
raise NotImplementedError(f"Unsupported DB type '{db_engine['db_type']}'.")
|
|
1387
|
+
|
|
1388
|
+
query_count = 0
|
|
1389
|
+
|
|
1390
|
+
if db_engine["db_type"] == "postgres":
|
|
1391
|
+
schema_name = db_engine.get("schema_name")
|
|
1392
|
+
connection_string = os.getenv(db_engine["connection_string_env_var"])
|
|
1393
|
+
if not connection_string:
|
|
1394
|
+
raise ValueError("Missing connection string.")
|
|
1395
|
+
|
|
1396
|
+
# Optional: get query timeout from config
|
|
1397
|
+
query_timeout = db_engine.get("query_timeout", 90)
|
|
1398
|
+
|
|
1399
|
+
query_count = asyncio.run(
|
|
1400
|
+
postgres_run_execution_async(
|
|
1401
|
+
connection_string, schema_name, predictions_path, num_threads, query_timeout, force_rerun
|
|
1402
|
+
)
|
|
1403
|
+
)
|
|
1404
|
+
|
|
1405
|
+
elif db_engine["db_type"] == "sqlite":
|
|
1406
|
+
db_folder = benchmark_info["db_engine"]["db_folder"]
|
|
1407
|
+
query_count = asyncio.run(
|
|
1408
|
+
sqlite_run_execution_async(db_folder, predictions_path, force_rerun=force_rerun)
|
|
1409
|
+
)
|
|
1410
|
+
|
|
1411
|
+
elif db_engine["db_type"] == "db2":
|
|
1412
|
+
schema_name = db_engine.get("schema_name")
|
|
1413
|
+
connection_string = os.getenv(db_engine["connection_string_env_var"])
|
|
1414
|
+
if not connection_string:
|
|
1415
|
+
raise ValueError("Missing DB2 connection string.")
|
|
1416
|
+
query_count = asyncio.run(
|
|
1417
|
+
db2_run_execution_async(
|
|
1418
|
+
connection_string, schema_name, predictions_path, num_threads, force_rerun
|
|
1419
|
+
)
|
|
1420
|
+
)
|
|
1421
|
+
elif db_engine["db_type"] == "mysql":
|
|
1422
|
+
connection_string = os.getenv(db_engine["connection_string_env_var"])
|
|
1423
|
+
if not connection_string:
|
|
1424
|
+
raise ValueError("Missing MySQL connection string.")
|
|
1425
|
+
|
|
1426
|
+
# Optional: get query timeout from config
|
|
1427
|
+
query_timeout = db_engine.get("query_timeout", 90)
|
|
1428
|
+
|
|
1429
|
+
query_count = asyncio.run(
|
|
1430
|
+
mysql_run_execution_async(
|
|
1431
|
+
connection_string, predictions_path, num_threads, query_timeout, force_rerun
|
|
1432
|
+
)
|
|
1433
|
+
)
|
|
1434
|
+
elif db_engine["db_type"] == "presto":
|
|
1435
|
+
connection_string = os.getenv(db_engine["connection_string_env_var"])
|
|
1436
|
+
if not connection_string:
|
|
1437
|
+
raise ValueError("Missing Presto connection string.")
|
|
1438
|
+
|
|
1439
|
+
# Optional: get query timeout from config
|
|
1440
|
+
query_timeout = db_engine.get("query_timeout", 300)
|
|
1441
|
+
|
|
1442
|
+
query_count = asyncio.run(
|
|
1443
|
+
presto_run_execution_async(
|
|
1444
|
+
connection_string, predictions_path, num_threads, query_timeout
|
|
1445
|
+
)
|
|
1446
|
+
)
|
|
1447
|
+
|
|
1448
|
+
logger.info(f"Total SQL queries executed: {query_count}")
|