]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
fall back to SHOW VARIABLES for MySQL < 5.6
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 4 Feb 2022 02:58:14 +0000 (21:58 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 4 Feb 2022 17:38:53 +0000 (12:38 -0500)
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

doc/build/changelog/unreleased_14/7518.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/mysql/base.py
lib/sqlalchemy/dialects/mysql/pyodbc.py
test/dialect/mysql/test_dialect.py
test/orm/test_expire.py

diff --git a/doc/build/changelog/unreleased_14/7518.rst b/doc/build/changelog/unreleased_14/7518.rst
new file mode 100644 (file)
index 0000000..bb5a9bc
--- /dev/null
@@ -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.
index 705909f4d5fa4584934de7005f443edc8b27a708..7ec2b3dc2647747d563d82b326ce79563de1ca81 100644 (file)
@@ -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."""
index d5a5c0c9dc4527f2e4b8aa47892d70e65466997f..22d60bd15350031539fe9cf90a2b4e67187ed9c2 100644 (file)
@@ -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:
index a96ea8cb489c0511e798c154f49ace2feddad344..4fa524a35c7bd94b5446f39412cf0ec592f5a453 100644 (file)
@@ -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"
index 58378956f26d198b520237fc9ec2651336152ff9..a5fd7533e9dd3e96b709ee36748b1423a968426b 100644 (file)
@@ -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)