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