Skip to content

fix(mysql-loader): refresh REPEATABLE READ snapshot before each query - #381

Merged
Chenglong Wang (Chenglong-MS) merged 2 commits into
microsoft:devfrom
chanxiusun:fix/mysql-loader-stale-snapshot
Jul 17, 2026
Merged

fix(mysql-loader): refresh REPEATABLE READ snapshot before each query#381
Chenglong Wang (Chenglong-MS) merged 2 commits into
microsoft:devfrom
chanxiusun:fix/mysql-loader-stale-snapshot

Conversation

@chanxiusun

Copy link
Copy Markdown
Contributor

Summary

The MySQLDataLoader keeps a long-lived PyMySQL connection with the default isolation level (REPEATABLE READ) and autocommit=False. The first SELECT against such a connection establishes a read view, and subsequent SELECTs keep returning rows from that snapshot even after other sessions have committed new rows.

That made /api/connectors/refresh-data behave as if a connector table never changed unless the Data Formulator process was restarted, breaking the "refresh table" affordance in the UI.

Fix

Call conn.commit() before every cursor.execute() so any open implicit transaction is rolled forward and the next SELECT establishes a fresh read view, picking up rows committed by other sessions in the meantime.

Verification

  • Started Data Formulator 0.7.0 against a local MySQL 8.0 (world.city with 4079 rows).
  • Inserted a new row from a separate PyMySQL session (ID=88888).
  • Slept 30s without restarting the server, then called POST /api/connectors/refresh-data.
  • row_count went from 4079 to 4080 and data_changed=true. The re-written world_city.parquet now contains ID=88888.

Risk

Low. conn.commit() on a connection that has not started a transaction is a no-op, and no transaction is in flight when cur.execute is about to start a new one. The change does not affect any other loader (only the MySQL path).

The MySQLDataLoader keeps a long-lived PyMySQL connection with the
default isolation level (REPEATABLE READ) and autocommit disabled. The
first SELECT in such a session establishes a read view, and subsequent
SELECTs against the same connection keep returning rows from that
snapshot even after other sessions have committed new rows. As a
result, /api/connectors/refresh-data would not see newly inserted rows
unless the Data Formulator process was restarted, which broke the
'refresh connector table' UI affordance.

Force a commit before every cursor.execute so that any open implicit
transaction is rolled forward and the next SELECT establishes a new
read view, picking up concurrent commits.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes stale reads from MySQLDataLoader caused by MySQL鈥檚 default REPEATABLE READ behavior on a long-lived connection by rolling forward the implicit transaction before executing each query, so subsequent reads observe rows committed by other sessions (e.g., for /api/connectors/refresh-data).

Changes:

  • Commit the MySQL connection before each cursor.execute() in _read_sql to force a fresh snapshot for subsequent SELECTs.

Comment on lines +135 to +138
try:
conn.commit()
except Exception:
pass
Comment on lines +132 to +136
# Force a fresh REPEATABLE READ snapshot on every read so rows
# committed by other sessions after this loader was first
# instantiated are visible without restarting the process.
try:
conn.commit()
@Chenglong-MS
Chenglong Wang (Chenglong-MS) merged commit 7a9154e into microsoft:dev Jul 17, 2026
1 check passed
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.

3 participants