Skip to content

fix(mysql): resolve wire-protocol column types and mutate rows from immutable results#42203

Open
massucattoj wants to merge 1 commit into
apache:masterfrom
massucattoj:fix/mysql-wire-protocol-column-types
Open

fix(mysql): resolve wire-protocol column types and mutate rows from immutable results#42203
massucattoj wants to merge 1 commit into
apache:masterfrom
massucattoj:fix/mysql-wire-protocol-column-types

Conversation

@massucattoj

Copy link
Copy Markdown
Contributor

SUMMARY

Columns of MySQL virtual datasets (created from SQL Lab queries) showed a "?" type icon in the chart panel instead of ABC/#, which also hid type-dependent controls such as X-Axis Sort By.

Physical datasets get column types from the SQLAlchemy inspector as DDL names (VARCHAR(100), DECIMAL(15,2)), which match MySQLEngineSpec.column_type_mappings. Virtual dataset columns instead get their type from the driver's cursor description via get_datatype, which returns the raw wire-protocol FIELD_TYPE constant names (VAR_STRING for VARCHAR, NEWDECIMAL for DECIMAL, TINY for TINYINT, BLOB for TEXT, and so on). None of those names matched any mapping regex, so type_generic resolved to None.

This PR adds mappings for the 8 wire-protocol names that had no match: VAR_STRING, NEWDECIMAL, TINY, SHORT, BLOB, YEAR, ENUM and SET. Regexes are anchored so they don't collide with existing DDL patterns (^tiny$ vs ^tinytext), and ^enum\b/^set\b also cover the DDL forms (ENUM('a','b')), which were equally unresolved.

Fixing the type resolution exposed a second, latent bug: MySQL registers a column_type_mutators entry that converts DECIMAL string values to Python Decimal, keyed on the resolved column type. It had never fired because NEWDECIMAL never resolved. Once it did, BaseEngineSpec.fetch_data crashed with TypeError: 'tuple' object does not support item assignment, because the mutation loop assigns rows back into the fetchall() result and mysqlclient returns a tuple of rows (psycopg2 returns a list, which is why the same code works for Postgres). Fixed by copying the result into a list before mutating.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

Screenshot 2026-07-18 at 12 59 02

After:
Screenshot 2026-07-18 at 12 59 27

TESTING INSTRUCTIONS

  1. Start a MySQL instance and create a table exercising the affected types:
    CREATE TABLE sample_data (
      id INT AUTO_INCREMENT PRIMARY KEY,
      label VARCHAR(100),
      amount DECIMAL(15, 2)
    );
    INSERT INTO sample_data (label, amount) VALUES ('Ships', 10.50), ('Planes', 22.30);
  2. Connect the MySQL database to Superset.
  3. In SQL Lab, run SELECT * FROM sample_data and use "Save or Overwrite Dataset" to save it as a new (virtual) dataset.
  4. Create a chart from that dataset.
  5. Verify label shows the string indicator, amount and id show the numeric indicator, and the chart renders data instead of a type error.

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags:
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
    • Migration is atomic, supports rollback & is backwards-compatible
    • Confirm DB migration upgrade and downgrade tested
    • Runtime estimates and downtime expectations provided
  • Introduces new feature or API
  • Removes existing feature or API

@dosubot dosubot Bot added change:backend Requires changing the backend data:connect:mysql Related to MySQL labels Jul 18, 2026
@bito-code-review

bito-code-review Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #782d8a

Actionable Suggestions - 0
Review Details
  • Files reviewed - 3 · Commit Range: cb76e5a..cb76e5a
    • superset/db_engine_specs/base.py
    • superset/db_engine_specs/mysql.py
    • tests/unit_tests/db_engine_specs/test_mysql.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

)
}
if column_mutators:
data = list(data)

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.

Suggestion: Unconditionally copying data to a new list when mutators are present duplicates the full result set in memory even when fetchall() already returned a mutable list. On large result sets this can cause avoidable memory spikes and potential worker OOMs; only copy when the returned container is immutable (for example, a tuple) so mutable list results can be mutated in place. [performance]

Severity Level: Major ⚠️
- ❌ Large Postgres queries may double memory, risking OOM.
- ⚠️ SQL Lab queries (`sql_lab.py:303`) duplicate result lists.
- ⚠️ Datasource introspection (`sqla/utils.py:162`) duplicates result containers.
- ⚠️ Chart data fetching (`models/core.py:865`) uses fetch_data.
Steps of Reproduction ✅
1. Configure a Postgres database using psycopg2 and a table with an INTERVAL column;
Superset uses `PostgresEngineSpec.column_type_mutators` for INTERVAL (see
`superset/db_engine_specs/postgres.py:26-28`).

2. In SQL Lab, run a query returning many rows including that INTERVAL column; the
execution path calls `db_engine_spec.fetch_data(cursor, increased_limit)` at
`superset/sql_lab.py:303`.

3. Inside `BaseEngineSpec.fetch_data` (`superset/db_engine_specs/base.py:13-49`),
`cursor.fetchall()` returns a list of row tuples for psycopg2, and `column_mutators` is
built; because an INTERVAL column is present, `column_mutators` is non-empty and the
mutator branch is taken.

4. The mutator branch executes `data = list(data)` at
`superset/db_engine_specs/base.py:39-41`, creating a second full list copy even though
`data` is already a list; for large result sets this doubles in-memory row storage
temporarily, which can cause worker memory spikes or OOM failures when mutating rows.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/db_engine_specs/base.py
**Line:** 1239:1239
**Comment:**
	*Performance: Unconditionally copying `data` to a new list when mutators are present duplicates the full result set in memory even when `fetchall()` already returned a mutable list. On large result sets this can cause avoidable memory spikes and potential worker OOMs; only copy when the returned container is immutable (for example, a tuple) so mutable list results can be mutated in place.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@bito-code-review

Copy link
Copy Markdown
Contributor

The flagged performance issue is correct. Unconditionally calling list(data) creates a full copy of the result set in memory, which can lead to memory spikes or OOM errors for large queries. To resolve this, you should only convert data to a list if it is not already a mutable sequence (e.g., if it is a tuple).

Here is the proposed fix for superset/db_engine_specs/base.py:

            if column_mutators:
                if not isinstance(data, list):
                    data = list(data)
                indexes = {row[0]: idx for idx, row in enumerate(description)}

This change ensures that if fetchall() already returns a list (as is common with many DB drivers), the memory-intensive copy is avoided. I have checked the other comments on this PR, and there are no other actionable review comments to address. Would you like me to proceed with any other changes?

superset/db_engine_specs/base.py

if column_mutators:
                if not isinstance(data, list):
                    data = list(data)
                indexes = {row[0]: idx for idx, row in enumerate(description)}

@codecov

codecov Bot commented Jul 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 65.08%. Comparing base (023b60c) to head (cb76e5a).

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #42203   +/-   ##
=======================================
  Coverage   65.07%   65.08%           
=======================================
  Files        2758     2758           
  Lines      155229   155230    +1     
  Branches    35572    35572           
=======================================
+ Hits       101020   101029    +9     
+ Misses      52294    52287    -7     
+ Partials     1915     1914    -1     
Flag Coverage Δ
hive 38.94% <0.00%> (-0.01%) ⬇️
mysql 57.64% <100.00%> (+0.01%) ⬆️
postgres 57.68% <0.00%> (-0.01%) ⬇️
presto 40.90% <0.00%> (-0.01%) ⬇️
python 59.11% <100.00%> (+0.01%) ⬆️
sqlite 57.29% <0.00%> (-0.01%) ⬇️
unit 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Labels

change:backend Requires changing the backend data:connect:mysql Related to MySQL size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant