fix(mysql): normalize string/bytes/Decimal values before boolean conversion
- Fix critical bug where bool('0') and bool(b'0') returned True instead of False
- Add proper type normalization for strings, bytes, and Decimal values
- Convert string/bytes/Decimal to int before applying bool() for accurate MySQL boolean conversion
- Maintains existing behavior for integer values while fixing edge cases
- Addresses feedback on conversion logic in mysql.py:323-330
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
diff --git a/superset/db_engine_specs/mysql.py b/superset/db_engine_specs/mysql.py
index 2bbe815..152cefd 100644
--- a/superset/db_engine_specs/mysql.py
+++ b/superset/db_engine_specs/mysql.py
@@ -320,7 +320,14 @@
new_row = list(row)
for col_idx in bool_column_indices:
if new_row[col_idx] is not None:
- new_row[col_idx] = bool(new_row[col_idx])
+ # Normalize different value types before boolean conversion
+ # bool("0") returns True, but we need False for MySQL boolean
+ value = new_row[col_idx]
+ if isinstance(value, (str, bytes)):
+ value = int(value)
+ elif isinstance(value, Decimal):
+ value = int(value)
+ new_row[col_idx] = bool(value)
converted_data.append(tuple(new_row))
return converted_data
diff --git a/tests/unit_tests/db_engine_specs/test_mysql.py b/tests/unit_tests/db_engine_specs/test_mysql.py
index 46e73cc..b5ad101 100644
--- a/tests/unit_tests/db_engine_specs/test_mysql.py
+++ b/tests/unit_tests/db_engine_specs/test_mysql.py
@@ -272,3 +272,146 @@
mock_cursor.description = description
assert MySQLEngineSpec.fetch_data(mock_cursor) == expected_result
+
+
+def test_fetch_data_boolean_integers() -> None:
+ """Test fetch_data converts integer 0/1 to boolean False/True."""
+ from superset.db_engine_specs.mysql import MySQLEngineSpec
+
+ mock_cursor = Mock()
+ mock_cursor.fetchall.return_value = [(1, "admin"), (0, "user")]
+ # TINYINT(1) column: type_code=1 (FIELD_TYPE.TINY), display_size=1
+ mock_cursor.description = [
+ ("is_active", 1, 1, 4, 3, 0, False), # TINYINT(1) - should convert
+ ("role", 254, 255, 0, 0, 0, False), # VARCHAR - should not convert
+ ]
+
+ result = MySQLEngineSpec.fetch_data(mock_cursor)
+ expected = [(True, "admin"), (False, "user")]
+ assert result == expected
+
+
+def test_fetch_data_boolean_strings() -> None:
+ """Test fetch_data converts string "0"/"1" to boolean False/True."""
+ from superset.db_engine_specs.mysql import MySQLEngineSpec
+
+ mock_cursor = Mock()
+ mock_cursor.fetchall.return_value = [("1", "admin"), ("0", "user")]
+ mock_cursor.description = [
+ ("is_active", 1, 1, 4, 3, 0, False), # TINYINT(1) - should convert
+ ("role", 254, 255, 0, 0, 0, False), # VARCHAR - should not convert
+ ]
+
+ result = MySQLEngineSpec.fetch_data(mock_cursor)
+ expected = [(True, "admin"), (False, "user")]
+ assert result == expected
+
+
+def test_fetch_data_boolean_bytes() -> None:
+ """Test fetch_data converts bytes b"0"/b"1" to boolean False/True."""
+ from superset.db_engine_specs.mysql import MySQLEngineSpec
+
+ mock_cursor = Mock()
+ mock_cursor.fetchall.return_value = [(b"1", "admin"), (b"0", "user")]
+ mock_cursor.description = [
+ ("is_active", 1, 1, 4, 3, 0, False), # TINYINT(1) - should convert
+ ("role", 254, 255, 0, 0, 0, False), # VARCHAR - should not convert
+ ]
+
+ result = MySQLEngineSpec.fetch_data(mock_cursor)
+ expected = [(True, "admin"), (False, "user")]
+ assert result == expected
+
+
+def test_fetch_data_boolean_decimals() -> None:
+ """Test fetch_data converts Decimal 0/1 to boolean False/True."""
+ from superset.db_engine_specs.mysql import MySQLEngineSpec
+
+ mock_cursor = Mock()
+ mock_cursor.fetchall.return_value = [
+ (Decimal("1"), "admin"),
+ (Decimal("0"), "user"),
+ ]
+ mock_cursor.description = [
+ ("is_active", 1, 1, 4, 3, 0, False), # TINYINT(1) - should convert
+ ("role", 254, 255, 0, 0, 0, False), # VARCHAR - should not convert
+ ]
+
+ result = MySQLEngineSpec.fetch_data(mock_cursor)
+ expected = [(True, "admin"), (False, "user")]
+ assert result == expected
+
+
+def test_fetch_data_boolean_with_nulls() -> None:
+ """Test fetch_data handles NULL values correctly in boolean columns."""
+ from superset.db_engine_specs.mysql import MySQLEngineSpec
+
+ mock_cursor = Mock()
+ mock_cursor.fetchall.return_value = [(1, "admin"), (None, "user"), (0, "guest")]
+ mock_cursor.description = [
+ ("is_active", 1, 1, 4, 3, 0, True), # TINYINT(1) with nulls - should convert
+ ("role", 254, 255, 0, 0, 0, False), # VARCHAR - should not convert
+ ]
+
+ result = MySQLEngineSpec.fetch_data(mock_cursor)
+ expected = [(True, "admin"), (None, "user"), (False, "guest")]
+ assert result == expected
+
+
+def test_fetch_data_boolean_mixed_columns() -> None:
+ """Test fetch_data with boolean and non-boolean columns together."""
+ from superset.db_engine_specs.mysql import MySQLEngineSpec
+
+ mock_cursor = Mock()
+ mock_cursor.fetchall.return_value = [(1, 50, 0), (0, 100, 1)]
+ mock_cursor.description = [
+ ("is_admin", 1, 1, 4, 3, 0, False), # TINYINT(1) - should convert
+ ("count", 3, 11, 4, 10, 0, False), # INT - should not convert
+ ("is_active", 1, 1, 4, 3, 0, False), # TINYINT(1) - should convert
+ ]
+
+ result = MySQLEngineSpec.fetch_data(mock_cursor)
+ expected = [(True, 50, False), (False, 100, True)]
+ assert result == expected
+
+
+def test_fetch_data_no_boolean_columns() -> None:
+ """Test fetch_data passes through data when no boolean columns present."""
+ from superset.db_engine_specs.mysql import MySQLEngineSpec
+
+ mock_cursor = Mock()
+ mock_cursor.fetchall.return_value = [(100, "test"), (200, "data")]
+ mock_cursor.description = [
+ ("count", 3, 11, 4, 10, 0, False), # INT - should not convert
+ ("name", 254, 255, 0, 0, 0, False), # VARCHAR - should not convert
+ ]
+
+ result = MySQLEngineSpec.fetch_data(mock_cursor)
+ expected = [(100, "test"), (200, "data")]
+ assert result == expected
+
+
+def test_fetch_data_boolean_mixed_driver_types() -> None:
+ """Test fetch_data with different driver return types in same dataset."""
+ from superset.db_engine_specs.mysql import MySQLEngineSpec
+
+ mock_cursor = Mock()
+ # Mix of integers, strings, bytes, decimals for boolean values
+ mock_cursor.fetchall.return_value = [
+ (1, "0", b"1"), # True, False, True
+ (0, "1", b"0"), # False, True, False
+ (Decimal(1), None, 0), # True, None, False
+ ]
+ mock_cursor.description = [
+ ("bool_int", 1, 1, 4, 3, 0, False), # TINYINT(1) - integers
+ ("bool_str", 1, 1, 4, 3, 0, False), # TINYINT(1) - strings
+ ("bool_bytes", 1, 1, 4, 3, 0, False), # TINYINT(1) - bytes
+ ]
+
+ result = MySQLEngineSpec.fetch_data(mock_cursor)
+ expected = [
+ (True, False, True),
+ (False, True, False),
+ (True, None, False),
+ ]
+ assert result == expected