Skip to content

Commit a33dfab

Browse files
committed
BUG27364914: Cursor prepared statements do not convert strings
Using the cursor with prepared statements, the returned values from queries are converted from binaries values to their correspond python values, like int and datetime types, but not for strings types which bytearrays are returned. This patch changes this behavior to return text values as string types. A test was added for regression. v5
1 parent 95d9a72 commit a33dfab

File tree

7 files changed

+103
-15
lines changed

7 files changed

+103
-15
lines changed

lib/mysql/connector/connection.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -469,8 +469,11 @@ def get_rows(self, count=None, binary=False, columns=None):
469469

470470
try:
471471
if binary:
472+
charset = self.charset
473+
if charset == 'utf8mb4':
474+
charset = 'utf8'
472475
rows = self._protocol.read_binary_result(
473-
self._socket, columns, count)
476+
self._socket, columns, count, charset)
474477
else:
475478
rows = self._protocol.read_text_result(self._socket,
476479
self._server_version,

lib/mysql/connector/conversion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,7 @@ def _STRING_to_python(self, value, dsc=None): # pylint: disable=C0103
561561
if dsc[7] & FieldFlag.BINARY:
562562
try:
563563
return value.decode(self.charset)
564-
except LookupError:
564+
except (LookupError, UnicodeDecodeError):
565565
return value
566566

567567
if self.charset == 'binary':

lib/mysql/connector/cursor.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1169,7 +1169,10 @@ def execute(self, operation, params=(), multi=False): # multi is unused
11691169
self._executed = operation
11701170
try:
11711171
if not isinstance(operation, bytes):
1172-
operation = operation.encode(self._connection.charset)
1172+
charset = self._connection.charset
1173+
if charset == 'utf8mb4':
1174+
charset = 'utf8'
1175+
operation = operation.encode(charset)
11731176
except (UnicodeDecodeError, UnicodeEncodeError) as err:
11741177
raise errors.ProgrammingError(str(err))
11751178

lib/mysql/connector/protocol.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ def _parse_binary_time(self, packet, field):
418418

419419
return (packet[length + 1:], tmp)
420420

421-
def _parse_binary_values(self, fields, packet):
421+
def _parse_binary_values(self, fields, packet, charset='utf-8'):
422422
"""Parse values from a binary result packet"""
423423
null_bitmap_length = (len(fields) + 7 + 2) // 8
424424
null_bitmap = [int(i) for i in packet[0:null_bitmap_length]]
@@ -446,11 +446,11 @@ def _parse_binary_values(self, fields, packet):
446446
values.append(value)
447447
else:
448448
(packet, value) = utils.read_lc_string(packet)
449-
values.append(value)
449+
values.append(value.decode(charset))
450450

451451
return tuple(values)
452452

453-
def read_binary_result(self, sock, columns, count=1):
453+
def read_binary_result(self, sock, columns, count=1, charset='utf-8'):
454454
"""Read MySQL binary protocol result
455455
456456
Reads all or given number of binary resultset rows from the socket.
@@ -470,7 +470,7 @@ def read_binary_result(self, sock, columns, count=1):
470470
values = None
471471
elif packet[4] == 0:
472472
eof = None
473-
values = self._parse_binary_values(columns, packet[5:])
473+
values = self._parse_binary_values(columns, packet[5:], charset)
474474
if eof is None and values is not None:
475475
rows.append(values)
476476
elif eof is None and values is None:
@@ -626,6 +626,8 @@ def make_stmt_execute(self, statement_id, data=(), parameters=(),
626626
values = []
627627
types = []
628628
packed = b''
629+
if charset == 'utf8mb4':
630+
charset = 'utf8'
629631
if long_data_used is None:
630632
long_data_used = {}
631633
if parameters and data:

tests/test_bugs.py

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2500,8 +2500,7 @@ def test_prepared_statement(self):
25002500
stmt = "INSERT INTO {0} VALUES (?,?,?)".format(
25012501
self.table)
25022502
data = [(1, b'bytes', '1234'), (2, u'aaaаффф', '1111')]
2503-
exp = [(1, b'bytes', b'1234'),
2504-
(2, u'aaaаффф'.encode('cp1251'), b'1111')]
2503+
exp = [(1, 'bytes', '1234'), (2, u'aaaаффф', '1111')]
25052504
cur.execute(stmt, data[0])
25062505
self.cnx.commit()
25072506
cur.execute("SELECT * FROM {0}".format(self.table))
@@ -3078,8 +3077,8 @@ def test_null(self):
30783077
"VALUES (?, ?, ?, ?, ?, ?, ?)".format(self.tbl))
30793078
params = (100, None, 'foo', None, datetime(2014, 8, 4, 9, 11, 14),
30803079
10, 'bar')
3081-
exp = (100, None, bytearray(b'foo'), None,
3082-
datetime(2014, 8, 4, 9, 11, 14), 10, bytearray(b'bar'))
3080+
exp = (100, None, 'foo', None,
3081+
datetime(2014, 8, 4, 9, 11, 14), 10, 'bar')
30833082
cur.execute(sql, params)
30843083

30853084
sql = "SELECT * FROM {0}".format(self.tbl)
@@ -4549,3 +4548,84 @@ def test_execute_get_json_type_as_str(self):
45494548
self.assertTrue(isinstance(col, STRING_TYPES),
45504549
"{} is type {} and not the expected type "
45514550
"string".format(col, type(col)))
4551+
4552+
4553+
class BugOra27364914(tests.MySQLConnectorTests):
4554+
"""BUG#27364914: CURSOR PREPARED STATEMENTS DO NOT CONVERT STRINGS
4555+
"""
4556+
charsets_list = ('gbk', 'sjis', 'big5', 'utf8', 'utf8mb4', 'latin1')
4557+
4558+
def setUp(self):
4559+
cnx = connection.MySQLConnection(**tests.get_mysql_config())
4560+
cur = cnx.cursor(cursor_class=cursor.MySQLCursorPrepared)
4561+
4562+
for charset in self.charsets_list:
4563+
tablename = '{0}_ps_test'.format(charset)
4564+
cur.execute("DROP TABLE IF EXISTS {0}".format(tablename))
4565+
table = (
4566+
"CREATE TABLE {table} ("
4567+
" id INT AUTO_INCREMENT KEY,"
4568+
" c1 VARCHAR(40),"
4569+
" val2 datetime"
4570+
") CHARACTER SET '{charset}'"
4571+
).format(table=tablename, charset=charset)
4572+
cur.execute(table)
4573+
cnx.commit()
4574+
cur.close()
4575+
cnx.close()
4576+
4577+
def tearDown(self):
4578+
cnx = connection.MySQLConnection(**tests.get_mysql_config())
4579+
for charset in self.charsets_list:
4580+
tablename = '{0}_ps_test'.format(charset)
4581+
cnx.cmd_query("DROP TABLE IF EXISTS {0}".format(tablename))
4582+
cnx.close()
4583+
4584+
def _test_charset(self, charset, data):
4585+
config = tests.get_mysql_config()
4586+
config['charset'] = charset
4587+
config['use_unicode'] = True
4588+
self.cnx = connection.MySQLConnection(**tests.get_mysql_config())
4589+
cur = self.cnx.cursor(cursor_class=cursor.MySQLCursorPrepared)
4590+
4591+
tablename = '{0}_ps_test'.format(charset)
4592+
cur.execute("TRUNCATE {0}".format(tablename))
4593+
self.cnx.commit()
4594+
4595+
insert = "INSERT INTO {0} (c1) VALUES (%s)".format(tablename)
4596+
for value in data:
4597+
cur.execute(insert, (value,))
4598+
self.cnx.commit()
4599+
4600+
cur.execute("SELECT id, c1 FROM {0} ORDER BY id".format(tablename))
4601+
for row in cur:
4602+
self.assertTrue(isinstance(row[1], STRING_TYPES),
4603+
"The value is expected to be a string")
4604+
self.assertEqual(data[row[0] - 1], row[1])
4605+
4606+
cur.close()
4607+
self.cnx.close()
4608+
4609+
@foreach_cnx()
4610+
def test_cursor_prepared_statement_with_charset_gbk(self):
4611+
self._test_charset('gbk', [u'赵孟頫', u'赵\孟\頫\\', u'遜'])
4612+
4613+
@foreach_cnx()
4614+
def test_cursor_prepared_statement_with_charset_sjis(self):
4615+
self._test_charset('sjis', ['\u005c'])
4616+
4617+
@foreach_cnx()
4618+
def test_cursor_prepared_statement_with_charset_big5(self):
4619+
self._test_charset('big5', ['\u5C62'])
4620+
4621+
@foreach_cnx()
4622+
def test_cursor_prepared_statement_with_charset_utf8mb4(self):
4623+
self._test_charset('utf8mb4', ['\u5C62'])
4624+
4625+
@foreach_cnx()
4626+
def test_cursor_prepared_statement_with_charset_utf8(self):
4627+
self._test_charset('utf8', [u'データベース', u'데이터베이스'])
4628+
4629+
@foreach_cnx()
4630+
def test_cursor_prepared_statement_with_charset_latin1(self):
4631+
self._test_charset('latin1', [u'ñ', u'Ñ'])

tests/test_cursor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1232,7 +1232,7 @@ def test_executemany(self):
12321232
self.assertEqual(3, cur.rowcount)
12331233

12341234
cur.execute(stmt_select)
1235-
self.assertEqual([(1, b'100'), (2, b'200'), (3, b'300')],
1235+
self.assertEqual([(1, '100'), (2, '200'), (3, '300')],
12361236
cur.fetchall(), "Multi insert test failed")
12371237

12381238
data = [(2,), (3,)]

tests/test_protocol.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -408,9 +408,9 @@ def test__parse_binary_values(self):
408408
b'\x00\x0a\x00\x00\x00\x10\x0f\x1e\x70\x82\x03\x00')
409409

410410
# float/double are returned as DECIMAL by MySQL
411-
exp = (bytearray(b'abc'),
412-
bytearray(b'3.14'),
413-
bytearray(b'-3.14159'),
411+
exp = ('abc',
412+
'3.14',
413+
'-3.14159',
414414
datetime.date(2003, 1, 31),
415415
datetime.datetime(1977, 6, 14, 21, 33, 14),
416416
datetime.timedelta(10, 58530, 230000),

0 commit comments

Comments
 (0)