--- /dev/null
+.. 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.
]
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
self, server_ansiquotes=self._server_ansiquotes
)
- default.DefaultDialect.initialize(self, connection)
-
self.supports_sequences = (
self.is_mariadb and self.server_version_info >= (10, 3)
)
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()
"""
# 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
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."""
# 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:
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
__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
):
)
def test_no_show_variables(self):
- from sqlalchemy.testing import mock
engine = engines.testing_engine()
engine.connect()
def test_no_default_isolation_level(self):
- from sqlalchemy.testing import mock
engine = engines.testing_engine()
):
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"
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)