Skip to content

fix(mysql): identify reads by result set instead of the leading keyword - #573

Open
cyberspace-cs wants to merge 1 commit into
OpenDCAI:mainfrom
cyberspace-cs:fix/mysql-result-set-detection
Open

cyberspace-cs wants to merge 1 commit into
OpenDCAI:mainfrom
cyberspace-cs:fix/mysql-result-set-detection

Conversation

@cyberspace-cs

Copy link
Copy Markdown
Contributor

What

MySQLConnector.execute_query decided whether a statement was a read by sniffing its leading keyword:

if sql.strip().upper().startswith(('SELECT', 'SHOW', 'DESC', 'EXPLAIN')):
    ...
else:
    raise Exception("Write operations are not allowed in read-only mode")

The check runs after cursor.execute(), so the statement has already been sent to the server. It now asks the server instead — MySQL sets cursor.description for every statement that returns rows and leaves it None for DML:

if cursor.description is None:
    raise Exception("Write operations are not allowed in read-only mode")
rows = cursor.fetchall()
columns = [desc[0] for desc in cursor.description]

Why

SQLExecutabilityFilter / SQLExecutionFilter explicitly keep CTE queries — filter_select_sql accepts both select and with. The SQLite connector supports them because it already keys off cursor.description. MySQL did not, so the two backends disagreed on the same dataset:

statement filter_select_sql SQLiteConnector MySQLConnector (before)
SELECT a FROM t keep ✅ ✅
WITH x AS (SELECT a FROM t) SELECT a FROM x keep ✅ ❌ "Write operations are not allowed"
/* hint */ SELECT a FROM t keep ✅ ❌
-- note\nSELECT a FROM t keep ✅ ❌
(SELECT a FROM t) UNION (SELECT a FROM t) drop (phase 1) ✅ ❌
DELETE FROM t drop ✅ rejected ✅ rejected

Two consequences on a MySQL-backed text2sql pipeline:

  1. Silent data loss. Every CTE query is reported as success=False, so both filters discard it from the output dataset even though phase 1 just admitted it.
  2. Wrong evaluation results. batch_compare_queries runs gold and prediction through batch_execute_queries. A CTE-based prediction fails, the comparison falls back to equal=False, and a correct prediction is scored as wrong. CTEs are common in Spider/BIRD-style outputs, so this is not a rare shape.

The write guard is unchanged: a DML statement still has description is None and is still rejected, and SET SESSION TRANSACTION READ ONLY remains the primary defense in connect().

Tests

New test/cpu_only/test_mysql_connector_result_sets.py drives the connector with a fake pymysql cursor (no server needed). 5 of the 9 read cases fail on main, all pass after the change; the 4 write cases keep failing as before.

$ pytest test/cpu_only/test_mysql_connector_result_sets.py -q
13 passed

The read cases cover SELECT, lower-case / padded SELECT, WITH, WITH RECURSIVE, a /* */-prefixed and a ---prefixed read, a parenthesized UNION, SHOW, and EXPLAIN. The write cases cover DELETE, UPDATE, INSERT, and WITH ... DELETE (a CTE that still writes — accepted by phase 1, still rejected here).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant