8000 BUG#35503377: First connected to server v8, then any v5 connections f… · mysql/mysql-connector-python@0a29791 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0a29791

Browse files
committed
BUG#35503377: First connected to server v8, then any v5 connections fail with utf8mb4 charset
If multiple connections are simultaneously open, they share the same character set info [1], this works when using parallelly a set of v8 [2] connections or a group of v5 [3] connections, however, mixing them up might lead to an error - for instance, the bug ticket reports a case when two connections v8 and v5 are opened (in the mentioned order) and their lifespans overlap. [1] The class `CharacterSet` is defined as a static entity - from properties down to methods. [2]: Connections made to a MySQL server whose version >= 8.x.y [3]: Connections made to a MySQL server whose version == 5.x.y Change-Id: I11b584a2bd6c3a2d4b1c747ea9a6a7352baa4bcf
1 parent 025b7a4 commit 0a29791

File tree

9 files changed

+263
-91
lines changed

9 files changed

+263
-91
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ v8.2.0
1919
- BUG#35547876: C/Python 8.1.0 type check build fails in the pb2 branch
2020
- BUG#35544123: Kerberos unit tests configuration is outdated
2121
- BUG#35503506: Query on information_schema.columns returns bytes
22+
- BUG#35503377: First connected to server v8, then any v5 connections fail with utf8mb4 charset
2223
- BUG#35141645: Memory leak in the mysqlx C extension
2324

2425
v8.1.0

lib/mysql/connector/abstracts.py

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
)
8181
from .conversion import MySQLConverter, MySQLConverterBase
8282
from .errors import (
83+
DatabaseError,
8384
Error,
8485
InterfaceError,
8586
NotSupportedError,
@@ -221,6 +222,7 @@ def __init__(self) -> None:
221222

222223
self._consume_results: bool = False
223224
self._init_command: Optional[str] = None
225+
self._character_set: CharacterSet = CharacterSet()
224226

225227
def __enter__(self) -> MySQLConnectionAbstract:
226228
return self
@@ -586,20 +588,6 @@ def config(self, **kwargs: Any) -> None:
586588
"plugin"
587589
)
588590

589-
# Configure character set and collation
590-
if "charset" in config or "collation" in config:
591-
try:
592-
charset = config["charset"]
593-
del config["charset"]
594-
except KeyError:
595-
charset = None
596-
try:
597-
collation = config["collation"]
598-
del config["collation"]
599-
except KeyError:
600-
collation = None
601-
self._charset_id = CharacterSet.get_charset_info(charset, collation)[0]
602-
603591
# Set converter class
604592
try:
605593
self.set_converter_class(config["converter_class"])
@@ -1108,7 +1096,7 @@ def charset(self) -> str:
11081096
11091097
Returns a string.
11101098
"""
1111-
return CharacterSet.get_info(self._charset_id)[0]
1099+
return self._character_set.get_info(self._charset_id)[0]
11121100

11131101
@property
11141102
def python_charset(self) -> str:
@@ -1121,7 +1109,7 @@ def python_charset(self) -> str:
11211109
11221110
Returns a string.
11231111
"""
1124-
encoding = CharacterSet.get_info(self._charset_id)[0]
1112+
encoding = self._character_set.get_info(self._charset_id)[0]
11251113
if encoding in ("utf8mb4", "utf8mb3", "binary"):
11261114
return "utf8"
11271115
return encoding
@@ -1157,28 +1145,28 @@ def set_charset_collation(
11571145
self._charset_id,
11581146
charset_name,
11591147
collation_name,
1160-
) = CharacterSet.get_charset_info(charset)
1148+
) = self._character_set.get_charset_info(charset)
11611149
elif isinstance(charset, str):
11621150
(
11631151
self._charset_id,
11641152
charset_name,
11651153
collation_name,
1166-
) = CharacterSet.get_charset_info(charset, collation)
1154+
) = self._character_set.get_charset_info(charset, collation)
11671155
else:
11681156
raise ValueError(err_msg.format("charset"))
11691157
elif collation:
11701158
(
11711159
self._charset_id,
11721160
charset_name,
11731161
collation_name,
1174-
) = CharacterSet.get_charset_info(collation=collation)
1162+
) = self._character_set.get_charset_info(collation=collation)
11751163
else:
11761164
charset = DEFAULT_CONFIGURATION["charset"]
11771165
(
11781166
self._charset_id,
11791167
charset_name,
11801168
collation_name,
1181-
) = CharacterSet.get_charset_info(charset, collation=None)
1169+
) = self._character_set.get_charset_info(charset, collation=None)
11821170

11831171
self._execute_query(f"SET NAMES '{charset_name}' COLLATE '{collation_name}'")
11841172

@@ -1190,7 +1178,7 @@ def set_charset_collation(
11901178
pass
11911179

11921180
if self.converter:
1193-
self.converter.set_charset(charset_name)
1181+
self.converter.set_charset(charset_name, character_set=self._character_set)
11941182

11951183
@property
11961184
def collation(self) -> str:
@@ -1202,7 +1190,7 @@ def collation(self) -> str:
12021190
12031191
Returns a string.
12041192
"""
1205-
return CharacterSet.get_charset_info(self._charset_id)[2]
1193+
return self._character_set.get_charset_info(self._charset_id)[2]
12061194

12071195
@abstractmethod
12081196
def _do_handshake(self) -> Any:
@@ -1241,15 +1229,41 @@ def connect(self, **kwargs: Any) -> None:
12411229
arguments are given, it will use the already configured or default
12421230
values.
12431231
"""
1232+
# open connection using the default charset id
12441233
if kwargs:
12451234
self.config(**kwargs)
12461235

12471236
self.disconnect()
12481237
self._open_connection()
1249-
# Server does not allow to run any other statement different from ALTER
1250-
# when user's password has been expired.
1238+
1239+
charset, collation = (
1240+
kwargs.pop("charset", None),
1241+
kwargs.pop("collation", None),
1242+
)
1243+
if charset or collation:
1244+
self._charset_id = self._character_set.get_charset_info(charset, collation)[
1245+
0
1246+
]
1247+
12511248
if not self._client_flags & ClientFlag.CAN_HANDLE_EXPIRED_PASSWORDS:
12521249
self._post_connection()
1250+
else:
1251+
# the server does not allow to run any other statement different from
1252+
# ALTER when user's password has been expired - the server either
1253+
# disconnects the client or restricts the client to "sandbox mode" [1].
1254+
# [1]: https://dev.mysql.com/doc/refman/5.7/en/expired-password-handling.html
1255+
try:
1256+
self.set_charset_collation(charset=self._charset_id)
1257+
except DatabaseError:
1258+
# get out of sandbox mode - with no FOR user clause, the statement sets
1259+
# the password for the current user.
1260+
self.cmd_query(f"SET PASSWORD = '{self._password1 or self._password}'")
1261+
1262+
# Set charset and collation.
1263+
self.set_charset_collation(charset=self._charset_id)
1264+
1265+
# go back to sandbox mode.
1266+
self.cmd_query(f"ALTER USER CURRENT_USER() PASSWORD EXPIRE")
12531267

12541268
def reconnect(self, attempts: int = 1, delay: int = 0) -> None:
12551269
"""Attempt to reconnect to the MySQL server
@@ -1444,7 +1458,7 @@ def set_converter_class(self, convclass: Optional[Type[MySQLConverter]]) -> None
14441458
methods and members of conversion.MySQLConverter.
14451459
"""
14461460
if convclass and issubclass(convclass, MySQLConverterBase):
1447-
charset_name = CharacterSet.get_info(self._charset_id)[0]
1461+
charset_name = self._character_set.get_info(self._charset_id)[0]
14481462
self._converter_class = convclass
14491463
self.converter = convclass(charset_name, self._use_unicode)
14501464
self.converter.str_fallback = self._converter_str_fallback

lib/mysql/connector/connection.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@
5858
from .abstracts import MySQLConnectionAbstract
5959
from .authentication import MySQLAuthenticator, get_auth_plugin
6060
from .constants import (
61-
CharacterSet,
6261
ClientFlag,
6362
FieldType,
6463
ServerCmd,
@@ -210,7 +209,7 @@ def _do_handshake(self) -> None:
210209
if isinstance(server_version, (str, bytes, bytearray))
211210
else "Unknown"
212211
)
213-
CharacterSet.set_mysql_version(self._server_version)
212+
self._character_set.set_mysql_version(self._server_version)
214213

215214
if not handshake["capabilities"] & ClientFlag.SSL:
216215
if self._auth_plugin == "mysql_clear_password" and not self.is_secure:

lib/mysql/connector/connection_cext.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939

4040
from . import version
4141
from .abstracts import MySQLConnectionAbstract
42-
from .constants import CharacterSet, ClientFlag, FieldFlag, ServerFlag, ShutdownType
42+
from .constants import ClientFlag, FieldFlag, ServerFlag, ShutdownType
4343
from .conversion import MySQLConverter
4444
from .errors import (
4545
InterfaceError,
@@ -164,7 +164,7 @@ def _do_handshake(self) -> None:
164164
self._server_version = self._check_server_version(
165165
self._handshake["server_version_original"]
166166
)
167-
CharacterSet.set_mysql_version(self._server_version)
167+
self._character_set.set_mysql_version(self._server_version)
168168

169169
@property
170170
def _server_status(self) -> int:
@@ -230,7 +230,7 @@ def in_transaction(self) -> int:
230230
return self._server_status & ServerFlag.STATUS_IN_TRANS
231231

232232
def _open_connection(self) -> None:
233-
charset_name = CharacterSet.get_info(self._charset_id)[0]
233+
charset_name = self._character_set.get_info(self._charset_id)[0]
234234
# pylint: disable=c-extension-no-member
235235
self._cmysql = _mysql_connector.MySQL(
236236
buffered=self._buffered,

lib/mysql/connector/constants.py

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,7 @@ class ShutdownType(_Constants):
705705
}
706706

707707

708-
class CharacterSet(_Constants):
708+
class CharacterSet:
709709
"""MySQL supported character sets and collations
710710
711711
List of character sets with their collations supported by MySQL. This
@@ -716,28 +716,27 @@ class CharacterSet(_Constants):
716716
name of the used character set or collation.
717717
"""
718718

719-
# Use LTS character set as default
720-
desc: List[
721-
Optional[Tuple[str, str, bool]]
722-
] = MYSQL_CHARACTER_SETS_57 # type: ignore[assignment]
723-
mysql_version: Tuple[int, ...] = (5, 7)
724-
725719
# Multi-byte character sets which use 5c (backslash) in characters
726720
slash_charsets: Tuple[int, ...] = (1, 13, 28, 84, 87, 88)
727721

728-
@classmethod
729-
def set_mysql_version(cls, version: Tuple[int, ...]) -> None:
722+
def __init__(self) -> None:
723+
# Use LTS character set as default
724+
self._desc: List[
725+
Optional[Tuple[str, str, bool]]
726+
] = MYSQL_CHARACTER_SETS_57 # type: ignore[assignment]
727+
self._mysql_version: Tuple[int, ...] = (5, 7)
728+
729+
def set_mysql_version(self, version: Tuple[int, ...]) -> None:
730730
"""Set the MySQL major version and change the charset mapping if is 5.7.
731731
732732
Args:
733733
version (tuple): MySQL version tuple.
734734
"""
735-
cls.mysql_version = version[:2]
736-
if cls.mysql_version >= (8, 0):
737-
cls.desc = MYSQL_CHARACTER_SETS
735+
self._mysql_version = version[:2]
736+
if self._mysql_version >= (8, 0):
737+
self._desc = MYSQL_CHARACTER_SETS
738738

739-
@classmethod
740-
def get_info(cls, setid: int) -> Tuple[str, str]:
739+
def get_info(self, setid: int) -> Tuple[str, str]: # type: ignore[override]
741740
"""Retrieves character set information as tuple using an ID
742741
743742
Retrieves character set and collation information based on the
@@ -748,24 +747,22 @@ def get_info(cls, setid: int) -> Tuple[str, str]:
748747
Returns a tuple.
749748
"""
750749
try:
751-
return cls.desc[setid][0:2]
750+
return self._desc[setid][0:2]
752751
except IndexError:
753752
raise ProgrammingError(f"Character set '{setid}' unsupported") from None
754753

755-
@classmethod
756-
def get_desc(cls, name: int) -> str: # type: ignore[override]
754+
def get_desc(self, name: int) -> str: # type: ignore[override]
757755
"""Retrieves character set information as string using an ID
758756
759757
Retrieves character set and collation information based on the
760758
given MySQL ID.
761759
762760
Returns a tuple.
763761
"""
764-
charset, collation = cls.get_info(name)
762+
charset, collation = self.get_info(name)
765763
return f"{charset}/{collation}"
766764

767-
@classmethod
768-
def get_default_collation(cls, charset: Union[int, str]) -> Tuple[str, str, int]:
765+
def get_default_collation(self, charset: Union[int, str]) -> Tuple[str, str, int]:
769766
"""Retrieves the default collation for given character set
770767
771768
Raises ProgrammingError when character set is not supported.
@@ -774,24 +771,23 @@ def get_default_collation(cls, charset: Union[int, str]) -> Tuple[str, str, int]
774771
"""
775772
if isinstance(charset, int):
776773
try:
777-
info = cls.desc[charset]
774+
info = self._desc[charset]
778775
return info[1], info[0], charset
779776
except (IndexError, KeyError) as err:
780777
raise ProgrammingError(
781778
f"Character set ID '{charset}' unsupported"
782779
) from err
783780

784-
for cid, info in enumerate(cls.desc):
781+
for cid, info in enumerate(self._desc):
785782
if info is None:
786783
continue
787784
if info[0] == charset and info[2] is True:
788785
return info[1], info[0], cid
789786

790787
raise ProgrammingError(f"Character set '{charset}' unsupported")
791788

792-
@classmethod
793789
def get_charset_info(
794-
cls, charset: Optional[Union[int, str]] = None, collation: Optional[str] = None
790+
self, charset: Optional[Union[int, str]] = None, collation: Optional[str] = None
795791
) -> Tuple[int, str, str]:
796792
"""Get character set information using charset name and/or collation
797793
@@ -811,39 +807,38 @@ def get_charset_info(
811807
info: Optional[Union[Tuple[str, str, bool], Tuple[str, str, int]]] = None
812808
if isinstance(charset, int):
813809
try:
814-
info = cls.desc[charset]
810+
info = self._desc[charset]
815811
return (charset, info[0], info[1])
816812
except IndexError as err:
817813
raise ProgrammingError(f"Character set ID {charset} unknown") from err
818814

819-
if charset in ("utf8", "utf-8") and cls.mysql_version >= (8, 0):
815+
if charset in ("utf8", "utf-8") and self._mysql_version >= (8, 0):
820816
charset = "utf8mb4"
821817
if charset is not None and collation is None:
822-
info = cls.get_default_collation(charset)
818+
info = self.get_default_collation(charset)
823819
return (info[2], info[1], info[0])
824820
if charset is None and collation is not None:
825-
for cid, info in enumerate(cls.desc):
821+
for cid, info in enumerate(self._desc):
826822
if info is None:
827823
continue
828824
if collation == info[1]:
829825
return (cid, info[0], info[1])
830826
raise ProgrammingError(f"Collation '{collation}' unknown")
831-
for cid, info in enumerate(cls.desc):
827+
for cid, info in enumerate(self._desc):
832828
if info is None:
833829
continue
834830
if info[0] == charset and info[1] == collation:
835831
return (cid, info[0], info[1])
836-
_ = cls.get_default_collation(charset)
832+
_ = self.get_default_collation(charset)
837833
raise ProgrammingError(f"Collation '{collation}' unknown")
838834

839-
@classmethod
840-
def get_supported(cls) -> Tuple[str, ...]:
835+
def get_supported(self) -> Tuple[str, ...]:
841836
"""Retrieves a list with names of all supproted character sets
842837
843838
Returns a tuple.
844839
"""
845840
res = []
846-
for info in cls.desc:
841+
for info in self._desc:
847842
if info and info[0] not in res:
848843
res.append(info[0])
849844
return tuple(res)

0 commit comments

Comments
 (0)
0