Files
sqlalchemy/test/dialect/mysql/test_dialect.py
T
valievkarim de399c914b Include new MySQL error code 4031 for MySQL disconnect check
Added disconnect code for MySQL error 4031, introduced in MySQL >= 8.0.24,
indicating connection idle timeout exceeded. In particular this repairs an
issue where pre-ping could not reconnect on a timed-out connection. Pull
request courtesy valievkarim.

Fixes: #8036
Closes: #8037
Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/8037
Pull-request-sha: 7ab605c2d2

Change-Id: I21249c9d8acb305ac43ce61b90b41daf7fabdfe8
2022-05-19 09:33:05 -04:00

615 lines
19 KiB
Python

# coding: utf-8
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
from sqlalchemy import MetaData
from sqlalchemy import select
from sqlalchemy import Table
from sqlalchemy import testing
from sqlalchemy.dialects import mysql
from sqlalchemy.engine.url import make_url
from sqlalchemy.testing import assert_raises_message
from sqlalchemy.testing import engines
from sqlalchemy.testing import eq_
from sqlalchemy.testing import expect_warnings
from sqlalchemy.testing import fixtures
from sqlalchemy.testing import in_
from sqlalchemy.testing import is_
from sqlalchemy.testing import mock
from sqlalchemy.testing.assertions import AssertsCompiledSQL
from .test_compiler import ReservedWordFixture
class BackendDialectTest(
ReservedWordFixture, fixtures.TestBase, AssertsCompiledSQL
):
__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
):
"""test #7167 - real backend level
We want to make sure that the "is mariadb" flag as well as the
correct identifier preparer are set up for dialects no matter how they
determine their "is_mariadb" flag.
"""
dialect = testing.db.dialect
expect_mariadb = testing.only_on("mariadb").enabled
table, expected_mysql, expected_mdb = mysql_mariadb_reserved_words
self.assert_compile(
select(table),
expected_mdb if expect_mariadb else expected_mysql,
dialect=dialect,
)
def test_no_show_variables(self):
engine = engines.testing_engine()
def my_execute(self, statement, *args, **kw):
if statement.startswith("SELECT @@"):
statement = "SELECT 1 FROM DUAL WHERE 1=0"
return real_exec(self, statement, *args, **kw)
real_exec = engine._connection_cls.exec_driver_sql
with mock.patch.object(
engine._connection_cls, "exec_driver_sql", my_execute
):
with expect_warnings(
"Could not retrieve SQL_MODE; please ensure the "
"MySQL user has permissions to SHOW VARIABLES"
):
engine.connect()
def test_no_default_isolation_level(self):
engine = engines.testing_engine()
real_isolation_level = testing.db.dialect.get_isolation_level
def fake_isolation_level(connection):
connection = mock.Mock(
cursor=mock.Mock(
return_value=mock.Mock(
fetchone=mock.Mock(return_value=None)
)
)
)
return real_isolation_level(connection)
with mock.patch.object(
engine.dialect, "get_isolation_level", fake_isolation_level
):
with expect_warnings(
"Could not retrieve transaction isolation level for MySQL "
"connection."
):
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"
)
assert c.exec_driver_sql("SELECT @@autocommit;").scalar()
c.rollback()
c = c.execution_options(isolation_level="READ COMMITTED")
assert not c.exec_driver_sql("SELECT @@autocommit;").scalar()
def test_isolation_level(self):
values = [
"READ UNCOMMITTED",
"READ COMMITTED",
"REPEATABLE READ",
"SERIALIZABLE",
]
for value in values:
c = testing.db.connect().execution_options(isolation_level=value)
eq_(testing.db.dialect.get_isolation_level(c.connection), value)
class DialectTest(fixtures.TestBase):
__backend__ = True
@testing.combinations(
(None, "cONnection was kILLEd", "InternalError", "pymysql", True),
(None, "cONnection aLREady closed", "InternalError", "pymysql", True),
(None, "something broke", "InternalError", "pymysql", False),
(1927, "Connection was killed", "OperationalError", "pymysql", True),
(1927, "Connection was killed", "OperationalError", "mysqldb", True),
(2006, "foo", "OperationalError", "mysqldb", True),
(2006, "foo", "OperationalError", "pymysql", True),
(2007, "foo", "OperationalError", "mysqldb", False),
(2007, "foo", "OperationalError", "pymysql", False),
(4031, "foo", "OperationalError", "mysqldb", True),
(4031, "foo", "OperationalError", "pymysql", True),
(4032, "foo", "OperationalError", "mysqldb", False),
(4032, "foo", "OperationalError", "pymysql", False),
)
def test_is_disconnect(
self, arg0, message, exc_cls_name, dialect_name, is_disconnect
):
class Error(Exception):
pass
dbapi = mock.Mock()
dbapi.Error = Error
dbapi.ProgrammingError = type("ProgrammingError", (Error,), {})
dbapi.OperationalError = type("OperationalError", (Error,), {})
dbapi.InterfaceError = type("InterfaceError", (Error,), {})
dbapi.InternalError = type("InternalError", (Error,), {})
dialect = getattr(mysql, dialect_name).dialect(dbapi=dbapi)
error = getattr(dbapi, exc_cls_name)(arg0, message)
eq_(dialect.is_disconnect(error, None, None), is_disconnect)
@testing.combinations(
("mysqldb"), ("pymysql"), id_="s", argnames="driver_name"
)
def test_ssl_arguments(self, driver_name):
url = (
"mysql+%s://scott:tiger@localhost:3306/test"
"?ssl_ca=/ca.pem&ssl_cert=/cert.pem&ssl_key=/key.pem" % driver_name
)
url_obj = make_url(url)
dialect = url_obj.get_dialect()()
expected = {
"{}".format(
"password" if driver_name == "pymysql" else "passwd"
): "tiger",
"{}".format(
"database" if driver_name == "pymysql" else "db"
): "test",
"ssl": {"ca": "/ca.pem", "cert": "/cert.pem", "key": "/key.pem"},
"host": "localhost",
"user": "scott",
"port": 3306,
}
# add check_hostname check for mysqldb and pymysql
if driver_name in ["mysqldb", "pymysql"]:
url = url + "&ssl_check_hostname=false"
expected["ssl"]["check_hostname"] = False
kwarg = dialect.create_connect_args(make_url(url))[1]
for k in ("use_unicode", "found_rows", "client_flag"):
kwarg.pop(k, None)
eq_(kwarg, expected)
@testing.combinations(
("compress", True),
("connect_timeout", 30),
("read_timeout", 30),
("write_timeout", 30),
("client_flag", 1234),
("local_infile", 1234),
("use_unicode", False),
("charset", "hello"),
)
def test_normal_arguments_mysqldb(self, kwarg, value):
from sqlalchemy.dialects.mysql import mysqldb
dialect = mysqldb.dialect()
connect_args = dialect.create_connect_args(
make_url(
"mysql+mysqldb://scott:tiger@localhost:3306/test"
"?%s=%s" % (kwarg, value)
)
)
eq_(connect_args[1][kwarg], value)
def test_mysqlconnector_buffered_arg(self):
from sqlalchemy.dialects.mysql import mysqlconnector
dialect = mysqlconnector.dialect()
kw = dialect.create_connect_args(
make_url("mysql+mysqlconnector://u:p@host/db?buffered=true")
)[1]
eq_(kw["buffered"], True)
kw = dialect.create_connect_args(
make_url("mysql+mysqlconnector://u:p@host/db?buffered=false")
)[1]
eq_(kw["buffered"], False)
kw = dialect.create_connect_args(
make_url("mysql+mysqlconnector://u:p@host/db")
)[1]
eq_(kw["buffered"], True)
def test_mysqlconnector_raise_on_warnings_arg(self):
from sqlalchemy.dialects.mysql import mysqlconnector
dialect = mysqlconnector.dialect()
kw = dialect.create_connect_args(
make_url(
"mysql+mysqlconnector://u:p@host/db?raise_on_warnings=true"
)
)[1]
eq_(kw["raise_on_warnings"], True)
kw = dialect.create_connect_args(
make_url(
"mysql+mysqlconnector://u:p@host/db?raise_on_warnings=false"
)
)[1]
eq_(kw["raise_on_warnings"], False)
kw = dialect.create_connect_args(
make_url("mysql+mysqlconnector://u:p@host/db")
)[1]
assert "raise_on_warnings" not in kw
@testing.only_on(
[
"mysql+mysqldb",
"mysql+pymysql",
"mariadb+mysqldb",
"mariadb+pymysql",
]
)
def test_random_arg(self):
dialect = testing.db.dialect
kw = dialect.create_connect_args(
make_url("mysql+mysqldb://u:p@host/db?foo=true")
)[1]
eq_(kw["foo"], "true")
@testing.only_on(
[
"mysql+mysqldb",
"mysql+pymysql",
"mariadb+mysqldb",
"mariadb+pymysql",
]
)
@testing.combinations(
("utf8mb4",),
("utf8",),
)
def test_special_encodings(self, enc):
eng = engines.testing_engine(
options={"connect_args": {"charset": enc, "use_unicode": 0}}
)
conn = eng.connect()
detected = conn.dialect._connection_charset
if enc == "utf8mb4":
eq_(detected, enc)
else:
in_(detected, ["utf8", "utf8mb3"])
@testing.only_on("mariadb+mariadbconnector")
def test_mariadb_connector_special_encodings(self):
eng = engines.testing_engine()
conn = eng.connect()
detected = conn.dialect._connection_charset
eq_(detected, "utf8mb4")
class ParseVersionTest(fixtures.TestBase):
def test_mariadb_madness(self):
mysql_dialect = make_url("mysql+mysqldb://").get_dialect()()
is_(mysql_dialect.is_mariadb, False)
mysql_dialect = make_url("mysql+pymysql://").get_dialect()()
is_(mysql_dialect.is_mariadb, False)
mariadb_dialect = make_url("mariadb://").get_dialect()()
is_(mariadb_dialect.is_mariadb, True)
mariadb_dialect = make_url("mariadb+pymysql://").get_dialect()()
is_(mariadb_dialect.is_mariadb, True)
assert_raises_message(
exc.InvalidRequestError,
"MySQL version 5.7.20 is not a MariaDB variant.",
mariadb_dialect._parse_server_version,
"5.7.20",
)
def test_502_minimum(self):
dialect = mysql.dialect()
assert_raises_message(
NotImplementedError,
"the MySQL/MariaDB dialect supports server "
"version info 5.0.2 and above.",
dialect._parse_server_version,
"5.0.1",
)
@testing.combinations(
((10, 2, 7), "10.2.7-MariaDB", (10, 2, 7), True),
(
(10, 2, 7),
"5.6.15.10.2.7-MariaDB",
(5, 6, 15, 10, 2, 7),
True,
),
((5, 0, 51, 24), "5.0.51a.24+lenny5", (5, 0, 51, 24), False),
((10, 2, 10), "10.2.10-MariaDB", (10, 2, 10), True),
((5, 7, 20), "5.7.20", (5, 7, 20), False),
((5, 6, 15), "5.6.15", (5, 6, 15), False),
(
(10, 2, 6),
"10.2.6.MariaDB.10.2.6+maria~stretch-log",
(10, 2, 6, 10, 2, 6),
True,
),
(
(10, 1, 9),
"10.1.9-MariaDBV1.0R050D002-20170809-1522",
(10, 1, 9, 20170809, 1522),
True,
),
)
def test_mariadb_normalized_version(
self, expected, raw_version, version, is_mariadb
):
dialect = mysql.dialect()
eq_(dialect._parse_server_version(raw_version), version)
dialect.server_version_info = version
eq_(dialect._mariadb_normalized_version_info, expected)
assert dialect._is_mariadb is is_mariadb
@testing.combinations(
(True, "10.2.7-MariaDB"),
(True, "5.6.15-10.2.7-MariaDB"),
(False, "10.2.10-MariaDB"),
(False, "5.7.20"),
(False, "5.6.15"),
(True, "10.2.6-MariaDB-10.2.6+maria~stretch.log"),
)
def test_mariadb_check_warning(self, expect_, version):
dialect = mysql.dialect(is_mariadb="MariaDB" in version)
dialect._parse_server_version(version)
if expect_:
with expect_warnings(
".*before 10.2.9 has known issues regarding "
"CHECK constraints"
):
dialect._warn_for_known_db_issues()
else:
dialect._warn_for_known_db_issues()
class RemoveUTCTimestampTest(fixtures.TablesTest):
"""This test exists because we removed the MySQL dialect's
override of the UTC_TIMESTAMP() function, where the commit message
for this feature stated that "it caused problems with executemany()".
Since no example was provided, we are trying lots of combinations
here.
[ticket:3966]
"""
__only_on__ = "mysql", "mariadb"
__backend__ = True
@classmethod
def define_tables(cls, metadata):
Table(
"t",
metadata,
Column("id", Integer, primary_key=True),
Column("x", Integer),
Column("data", DateTime),
)
Table(
"t_default",
metadata,
Column("id", Integer, primary_key=True),
Column("x", Integer),
Column("idata", DateTime, default=func.utc_timestamp()),
Column("udata", DateTime, onupdate=func.utc_timestamp()),
)
def test_insert_executemany(self, connection):
conn = connection
conn.execute(
self.tables.t.insert().values(data=func.utc_timestamp()),
[{"x": 5}, {"x": 6}, {"x": 7}],
)
def test_update_executemany(self, connection):
conn = connection
timestamp = datetime.datetime(2015, 4, 17, 18, 5, 2)
conn.execute(
self.tables.t.insert(),
[
{"x": 5, "data": timestamp},
{"x": 6, "data": timestamp},
{"x": 7, "data": timestamp},
],
)
conn.execute(
self.tables.t.update()
.values(data=func.utc_timestamp())
.where(self.tables.t.c.x == bindparam("xval")),
[{"xval": 5}, {"xval": 6}, {"xval": 7}],
)
def test_insert_executemany_w_default(self, connection):
conn = connection
conn.execute(
self.tables.t_default.insert(), [{"x": 5}, {"x": 6}, {"x": 7}]
)
def test_update_executemany_w_default(self, connection):
conn = connection
timestamp = datetime.datetime(2015, 4, 17, 18, 5, 2)
conn.execute(
self.tables.t_default.insert(),
[
{"x": 5, "idata": timestamp},
{"x": 6, "idata": timestamp},
{"x": 7, "idata": timestamp},
],
)
conn.execute(
self.tables.t_default.update()
.values(idata=func.utc_timestamp())
.where(self.tables.t_default.c.x == bindparam("xval")),
[{"xval": 5}, {"xval": 6}, {"xval": 7}],
)
class SQLModeDetectionTest(fixtures.TestBase):
__only_on__ = "mysql", "mariadb"
__backend__ = True
def _options(self, modes):
def connect(con, record):
cursor = con.cursor()
cursor.execute("set sql_mode='%s'" % (",".join(modes)))
e = engines.testing_engine(
options={
"pool_events": [
(connect, "first_connect"),
(connect, "connect"),
]
}
)
return e
def test_backslash_escapes(self):
engine = self._options(["NO_BACKSLASH_ESCAPES"])
c = engine.connect()
assert not engine.dialect._backslash_escapes
c.close()
engine.dispose()
engine = self._options([])
c = engine.connect()
assert engine.dialect._backslash_escapes
c.close()
engine.dispose()
def test_ansi_quotes(self):
engine = self._options(["ANSI_QUOTES"])
c = engine.connect()
assert engine.dialect._server_ansiquotes
c.close()
engine.dispose()
def test_combination(self):
engine = self._options(["ANSI_QUOTES,NO_BACKSLASH_ESCAPES"])
c = engine.connect()
assert engine.dialect._server_ansiquotes
assert not engine.dialect._backslash_escapes
c.close()
engine.dispose()
class ExecutionTest(fixtures.TestBase):
"""Various MySQL execution special cases."""
__only_on__ = "mysql", "mariadb"
__backend__ = True
def test_charset_caching(self):
engine = engines.testing_engine()
cx = engine.connect()
meta = MetaData()
charset = engine.dialect._detect_charset(cx)
meta.reflect(cx)
eq_(cx.dialect._connection_charset, charset)
cx.close()
def test_sysdate(self, connection):
d = connection.execute(func.sysdate()).scalar()
assert isinstance(d, datetime.datetime)