]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
narrow scope of _correct_for_mysql_bugs_88718_96365 rel_2_0
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 20 Apr 2026 13:30:35 +0000 (09:30 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 21 Apr 2026 14:58:45 +0000 (10:58 -0400)
Narrowed the scope of the internal workaround for MySQL bugs `#88718
<https://bugs.mysql.com/bug.php?id=88718>`_ and `#96365
<https://bugs.mysql.com/bug.php?id=96365>`_ so that it is only applied
where needed: MySQL 8.0.1 through 8.0.13 (where bug 88718 is present), and
on systems with ``lower_case_table_names=2`` (where bug 96365 applies,
typically macOS).  Previously the workaround was applied unconditionally
for all MySQL 8.0+ versions, which caused a ``KeyError`` during foreign key
reflection when the database user lacked SELECT privileges on referred
tables.

Fixes: #13243
Change-Id: I7c29f67d1653c5cd32f29e098f038fea1d56117b
(cherry picked from commit 530e1f71e74263ee9e23245071af2557aa65d425)

doc/build/changelog/unreleased_20/13243.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/mysql/base.py
test/dialect/mysql/test_dialect.py

diff --git a/doc/build/changelog/unreleased_20/13243.rst b/doc/build/changelog/unreleased_20/13243.rst
new file mode 100644 (file)
index 0000000..2a19b9e
--- /dev/null
@@ -0,0 +1,13 @@
+.. change::
+    :tags: bug, mysql, reflection
+    :tickets: 13243
+
+    Narrowed the scope of the internal workaround for MySQL bugs `#88718
+    <https://bugs.mysql.com/bug.php?id=88718>`_ and `#96365
+    <https://bugs.mysql.com/bug.php?id=96365>`_ so that it is only applied
+    where needed: MySQL 8.0.1 through 8.0.13 (where bug 88718 is present), and
+    on systems with ``lower_case_table_names=2`` (where bug 96365 applies,
+    typically macOS).  Previously the workaround was applied unconditionally
+    for all MySQL 8.0+ versions, which caused a ``KeyError`` during foreign key
+    reflection when the database user lacked SELECT privileges on referred
+    tables.
index bc8875efc0c151215b31167cd919d73567a0d20c..3c84e36fd38266dcab824e54cfd9bc0d12a4c73a 100644 (file)
@@ -2764,6 +2764,7 @@ class MySQLDialect(default.DefaultDialect):
     # i.e. first connect
     _backslash_escapes = True
     _server_ansiquotes = False
+    _casing = 0
 
     server_version_info: Tuple[int, ...]
     identifier_preparer: MySQLIdentifierPreparer
@@ -3186,9 +3187,9 @@ class MySQLDialect(default.DefaultDialect):
             self._is_mysql and self.server_version_info >= (8, 0, 1)
         )
 
-        self._needs_correct_for_88718_96365 = (
-            not self.is_mariadb and self.server_version_info >= (8,)
-        )
+        self._needs_correct_for_88718_96365 = self.server_version_info >= (
+            8,
+        ) and (self.server_version_info < (8, 0, 14) or self._casing == 2)
 
         self.delete_returning = (
             self.is_mariadb and self.server_version_info >= (10, 0, 5)
index 7e31c666f3a72798246c4defa5a46ded565d89b0..98ee19e54f329ba72d20ad50153085527de37501 100644 (file)
@@ -60,6 +60,42 @@ class BackendDialectTest(
 
             yield go
 
+    @testing.fixture
+    def mysql_version_casing_dialect(self, mysql_version_dialect):
+        """yield a MySQL engine that will simulate a specific version
+        and casing setting.
+
+        Builds on mysql_version_dialect, additionally patching out
+        _detect_casing and _detect_sql_mode so that
+        dialect.initialize() can be called with controlled values.
+
+        """
+        _patchers = []
+
+        def go(server_version, casing):
+            engine = mysql_version_dialect(server_version)
+
+            def _mock_detect_casing(conn):
+                engine.dialect._casing = casing
+
+            def _mock_detect_sql_mode(conn):
+                engine.dialect._sql_mode = ""
+
+            for method, fn in [
+                ("_detect_casing", _mock_detect_casing),
+                ("_detect_sql_mode", _mock_detect_sql_mode),
+            ]:
+                p = mock.patch.object(engine.dialect, method, fn)
+                p.start()
+                _patchers.append(p)
+
+            return engine
+
+        yield go
+
+        for p in _patchers:
+            p.stop()
+
     def test_reserved_words_mysql_vs_mariadb(
         self, mysql_mariadb_reserved_words
     ):
@@ -181,6 +217,29 @@ class BackendDialectTest(
             c = testing.db.connect().execution_options(isolation_level=value)
             eq_(testing.db.dialect.get_isolation_level(c.connection), value)
 
+    @testing.only_on("mysql")
+    @testing.combinations(
+        ("8.0.12", 0, True),
+        ("8.0.13", 0, True),
+        ("8.0.13", 2, True),
+        ("8.0.14", 0, False),
+        ("8.0.14", 1, False),
+        ("8.0.14", 2, True),
+        ("8.0.32", 0, False),
+        ("8.0.32", 2, True),
+        ("8.4.0", 0, False),
+        ("8.4.0", 2, True),
+        ("5.7.0", 0, False),
+        ("5.7.0", 2, False),
+        argnames="server_version,casing,expected",
+    )
+    def test_needs_correct_for_88718_96365(
+        self, mysql_version_casing_dialect, server_version, casing, expected
+    ):
+        engine = mysql_version_casing_dialect(server_version, casing)
+        engine.connect()
+        is_(engine.dialect._needs_correct_for_88718_96365, expected)
+
 
 class DialectTest(fixtures.TestBase):
     __backend__ = True