From: Mike Bayer Date: Mon, 20 Apr 2026 13:30:35 +0000 (-0400) Subject: narrow scope of _correct_for_mysql_bugs_88718_96365 X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=530e1f71e74263ee9e23245071af2557aa65d425;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git 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 --- 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 d2e7068d10..908d1b5cb0 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -2784,6 +2784,7 @@ class MySQLDialect(_mariadb_shim.MariaDBShim, default.DefaultDialect): # i.e. first connect _backslash_escapes = True _server_ansiquotes = False + _casing = 0 _support_default_function = True _support_float_cast = False @@ -3177,7 +3178,9 @@ class MySQLDialect(_mariadb_shim.MariaDBShim, default.DefaultDialect): self.use_mysql_for_share = self.server_version_info >= (8, 0, 1) - self._needs_correct_for_88718_96365 = 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._requires_alias_for_on_duplicate_key = ( self.server_version_info >= (8, 0, 20) diff --git a/test/dialect/mysql/test_dialect.py b/test/dialect/mysql/test_dialect.py index 41bf091c5a..eaf86c15d2 100644 --- a/test/dialect/mysql/test_dialect.py +++ b/test/dialect/mysql/test_dialect.py @@ -64,6 +64,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 ): @@ -185,6 +221,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