From e10faf27cd7a0044cef5ffe9c69a5dd043b8dea0 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 20 Apr 2026 09:30:35 -0400 Subject: [PATCH] narrow scope of _correct_for_mysql_bugs_88718_96365 Narrowed the scope of the internal workaround for MySQL bugs `#88718 `_ and `#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 | 13 +++++ lib/sqlalchemy/dialects/mysql/base.py | 7 +-- test/dialect/mysql/test_dialect.py | 59 +++++++++++++++++++++ 3 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 doc/build/changelog/unreleased_20/13243.rst diff --git a/doc/build/changelog/unreleased_20/13243.rst b/doc/build/changelog/unreleased_20/13243.rst new file mode 100644 index 0000000000..2a19b9e474 --- /dev/null +++ b/doc/build/changelog/unreleased_20/13243.rst @@ -0,0 +1,13 @@ +.. change:: + :tags: bug, mysql, reflection + :tickets: 13243 + + Narrowed the scope of the internal workaround for MySQL bugs `#88718 + `_ and `#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. diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index bc8875efc0..3c84e36fd3 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -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) diff --git a/test/dialect/mysql/test_dialect.py b/test/dialect/mysql/test_dialect.py index 7e31c666f3..98ee19e54f 100644 --- a/test/dialect/mysql/test_dialect.py +++ b/test/dialect/mysql/test_dialect.py @@ -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 -- 2.47.3