From: Mike Bayer Date: Fri, 4 Feb 2022 02:58:14 +0000 (-0500) Subject: fall back to SHOW VARIABLES for MySQL < 5.6 X-Git-Tag: rel_2_0_0b1~500^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9f1ed1c68af05eab5851ffd038011e3e3bd36b63;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git fall back to SHOW VARIABLES for MySQL < 5.6 Fixed regression caused by :ticket:`7518` where changing the syntax "SHOW VARIABLES" to "SELECT @@" broke compatibility with MySQL versions older than 5.6, including early 5.0 releases. While these are very old MySQL versions, a change in compatibility was not planned, so version-specific logic has been restored to fall back to "SHOW VARIABLES" for MySQL server versions < 5.6. includes unrelated orm/test_expire ordering issue , only showing up on 1.4 / py2.7 but seems to be passing by luck otherwise Fixes: #7518 Change-Id: Ia554080af742f2c3437f88cf3f7a4827b5e55da8 --- diff --git a/doc/build/changelog/unreleased_14/7518.rst b/doc/build/changelog/unreleased_14/7518.rst new file mode 100644 index 0000000000..bb5a9bc21b --- /dev/null +++ b/doc/build/changelog/unreleased_14/7518.rst @@ -0,0 +1,10 @@ +.. change:: + :tags: bug, mysql, regression + :tickets: 7518 + + Fixed regression caused by :ticket:`7518` where changing the syntax "SHOW + VARIABLES" to "SELECT @@" broke compatibility with MySQL versions older + than 5.6, including early 5.0 releases. While these are very old MySQL + versions, a change in compatibility was not planned, so version-specific + logic has been restored to fall back to "SHOW VARIABLES" for MySQL server + versions < 5.6. diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index 705909f4d5..7ec2b3dc26 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -2668,9 +2668,18 @@ class MySQLDialect(default.DefaultDialect): ] def initialize(self, connection): + # this is driver-based, does not need server version info + # and is fairly critical for even basic SQL operations self._connection_charset = self._detect_charset(connection) + + # call super().initialize() because we need to have + # server_version_info set up. in 1.4 under python 2 only this does the + # "check unicode returns" thing, which is the one area that some + # SQL gets compiled within initialize() currently + default.DefaultDialect.initialize(self, connection) + self._detect_sql_mode(connection) - self._detect_ansiquotes(connection) + self._detect_ansiquotes(connection) # depends on sql mode self._detect_casing(connection) if self._server_ansiquotes: # if ansiquotes == True, build a new IdentifierPreparer @@ -2679,8 +2688,6 @@ class MySQLDialect(default.DefaultDialect): self, server_ansiquotes=self._server_ansiquotes ) - default.DefaultDialect.initialize(self, connection) - self.supports_sequences = ( self.is_mariadb and self.server_version_info >= (10, 3) ) @@ -3066,6 +3073,23 @@ class MySQLDialect(default.DefaultDialect): sql = parser._describe_to_create(table_name, columns) return parser.parse(sql, charset) + def _fetch_setting(self, connection, setting_name): + charset = self._connection_charset + + if self.server_version_info and self.server_version_info < (5, 6): + sql = "SHOW VARIABLES LIKE '%s'" % setting_name + fetch_col = 1 + else: + sql = "SELECT @@%s" % setting_name + fetch_col = 0 + + show_var = connection.exec_driver_sql(sql) + row = self._compat_first(show_var, charset=charset) + if not row: + return None + else: + return row[fetch_col] + def _detect_charset(self, connection): raise NotImplementedError() @@ -3078,22 +3102,18 @@ class MySQLDialect(default.DefaultDialect): """ # https://dev.mysql.com/doc/refman/en/identifier-case-sensitivity.html - charset = self._connection_charset - show_var = connection.exec_driver_sql( - "SELECT @@lower_case_table_names" - ) - row = self._compat_first(show_var, charset=charset) - if not row: + setting = self._fetch_setting(connection, "lower_case_table_names") + if setting is None: cs = 0 else: # 4.0.15 returns OFF or ON according to [ticket:489] # 3.23 doesn't, 4.0.27 doesn't.. - if row[0] == "OFF": + if setting == "OFF": cs = 0 - elif row[0] == "ON": + elif setting == "ON": cs = 1 else: - cs = int(row[0]) + cs = int(setting) self._casing = cs return cs @@ -3111,19 +3131,16 @@ class MySQLDialect(default.DefaultDialect): return collations def _detect_sql_mode(self, connection): - row = self._compat_first( - connection.exec_driver_sql("SELECT @@sql_mode"), - charset=self._connection_charset, - ) + setting = self._fetch_setting(connection, "sql_mode") - if not row: + if setting is None: util.warn( "Could not retrieve SQL_MODE; please ensure the " "MySQL user has permissions to SHOW VARIABLES" ) self._sql_mode = "" else: - self._sql_mode = row[0] or "" + self._sql_mode = setting or "" def _detect_ansiquotes(self, connection): """Detect and adjust for the ANSI_QUOTES sql mode.""" diff --git a/lib/sqlalchemy/dialects/mysql/pyodbc.py b/lib/sqlalchemy/dialects/mysql/pyodbc.py index d5a5c0c9dc..22d60bd153 100644 --- a/lib/sqlalchemy/dialects/mysql/pyodbc.py +++ b/lib/sqlalchemy/dialects/mysql/pyodbc.py @@ -89,9 +89,7 @@ class MySQLDialect_pyodbc(PyODBCConnector, MySQLDialect): # If it's decided that issuing that sort of SQL leaves you SOL, then # this can prefer the driver value. try: - value = connection.exec_driver_sql( - "select @@character_set_client" - ).scalar() + value = self._fetch_setting("character_set_client") if value: return value except exc.DBAPIError: diff --git a/test/dialect/mysql/test_dialect.py b/test/dialect/mysql/test_dialect.py index a96ea8cb48..4fa524a35c 100644 --- a/test/dialect/mysql/test_dialect.py +++ b/test/dialect/mysql/test_dialect.py @@ -5,6 +5,7 @@ import datetime from sqlalchemy import bindparam from sqlalchemy import Column from sqlalchemy import DateTime +from sqlalchemy import event from sqlalchemy import exc from sqlalchemy import func from sqlalchemy import Integer @@ -32,6 +33,35 @@ class BackendDialectTest( __backend__ = True __only_on__ = "mysql", "mariadb" + @testing.fixture + def mysql_version_dialect(self, testing_engine): + """yield a MySQL engine that will simulate a specific version. + + patches out various methods to not fail + + """ + engine = testing_engine() + _server_version = [None] + with mock.patch.object( + engine.dialect, + "_get_server_version_info", + lambda conn: engine.dialect._parse_server_version( + _server_version[0] + ), + ), mock.patch.object( + engine.dialect, "_set_mariadb", lambda *arg: None + ), mock.patch.object( + engine.dialect, + "get_isolation_level", + lambda *arg: "REPEATABLE READ", + ): + + def go(server_version): + _server_version[0] = server_version + return engine + + yield go + def test_reserved_words_mysql_vs_mariadb( self, mysql_mariadb_reserved_words ): @@ -54,7 +84,6 @@ class BackendDialectTest( ) def test_no_show_variables(self): - from sqlalchemy.testing import mock engine = engines.testing_engine() @@ -74,7 +103,6 @@ class BackendDialectTest( engine.connect() def test_no_default_isolation_level(self): - from sqlalchemy.testing import mock engine = engines.testing_engine() @@ -99,6 +127,43 @@ class BackendDialectTest( ): engine.connect() + @testing.combinations( + "10.5.12-MariaDB", "5.6.49", "5.0.2", argnames="server_version" + ) + def test_variable_fetch(self, mysql_version_dialect, server_version): + """test #7518""" + engine = mysql_version_dialect(server_version) + + fetches = [] + + # the initialize() connection does not seem to use engine-level events. + # not changing that here + + @event.listens_for(engine, "do_execute_no_params") + @event.listens_for(engine, "do_execute") + def do_execute_no_params(cursor, statement, *arg): + if statement.startswith("SHOW VARIABLES") or statement.startswith( + "SELECT @@" + ): + fetches.append(statement) + return None + + engine.connect() + + if server_version == "5.0.2": + eq_( + fetches, + [ + "SHOW VARIABLES LIKE 'sql_mode'", + "SHOW VARIABLES LIKE 'lower_case_table_names'", + ], + ) + else: + eq_( + fetches, + ["SELECT @@sql_mode", "SELECT @@lower_case_table_names"], + ) + def test_autocommit_isolation_level(self): c = testing.db.connect().execution_options( isolation_level="AUTOCOMMIT" diff --git a/test/orm/test_expire.py b/test/orm/test_expire.py index 58378956f2..a5fd7533e9 100644 --- a/test/orm/test_expire.py +++ b/test/orm/test_expire.py @@ -1083,7 +1083,11 @@ class ExpireTest(_fixtures.FixtureTest): self.mapper_registry.map_imperatively( User, users, - properties={"addresses": relationship(Address, backref="user")}, + properties={ + "addresses": relationship( + Address, backref="user", order_by=addresses.c.id + ) + }, ) self.mapper_registry.map_imperatively(Address, addresses)