Skip to content

Commit 9055f9b

Browse files
committed
BUG29808262: Fix BLOB types conversion
MySQL stores TEXT types as BLOB and JSON as LONGBLOB. The TEXT and JSON types should be converted to str and the rest of the BLOB types as bytes. This patch apply these conversion rules. Tests were changed for regression.
1 parent e03eb3b commit 9055f9b

File tree

3 files changed

+73
-233
lines changed

3 files changed

+73
-233
lines changed

lib/mysql/connector/conversion.py

Lines changed: 7 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2009, 2018, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2009, 2020, Oracle and/or its affiliates. All rights reserved.
22
#
33
# This program is free software; you can redistribute it and/or modify
44
# it under the terms of the GNU General Public License, version 2.0, as
@@ -582,66 +582,6 @@ def _SET_to_python(self, value, dsc=None): # pylint: disable=C0103
582582
raise ValueError("Could not convert set %s to a sequence." % value)
583583
return set_type
584584

585-
def _JSON_to_python(self, value, dsc=None): # pylint: disable=C0103
586-
"""Returns JSON column type as python type
587-
588-
Returns JSON column type as python type.
589-
"""
590-
try:
591-
num = float(value)
592-
if num.is_integer():
593-
return int(value)
594-
return num
595-
except ValueError:
596-
pass
597-
598-
if value == b'true':
599-
return True
600-
elif value == b'false':
601-
return False
602-
603-
# The following types are returned between double quotes or
604-
# bytearray(b'"')[0] or int 34 for shortness.
605-
if value[0] == 34 and value[-1] == 34:
606-
value_nq = value[1:-1]
607-
608-
try:
609-
value_datetime = self._DATETIME_to_python(value_nq)
610-
if value_datetime is not None:
611-
return value_datetime
612-
except ValueError:
613-
pass
614-
try:
615-
value_date = self._DATE_to_python(value_nq)
616-
if value_date is not None:
617-
return value_date
618-
except ValueError:
619-
pass
620-
try:
621-
value_time = self._TIME_to_python(value_nq)
622-
if value_time is not None:
623-
return value_time
624-
except ValueError:
625-
pass
626-
627-
if isinstance(value, (bytes, bytearray)):
628-
return value.decode(self.charset)
629-
630-
if dsc is not None:
631-
# Check if we deal with a SET
632-
if dsc[7] & FieldFlag.SET:
633-
return self._SET_to_python(value, dsc)
634-
if dsc[7] & FieldFlag.BINARY:
635-
if self.charset != 'binary' and not isinstance(value, str):
636-
try:
637-
return value.decode(self.charset)
638-
except (LookupError, UnicodeDecodeError):
639-
return value
640-
else:
641-
return value
642-
643-
return self._STRING_to_python(value, dsc)
644-
645585
def _STRING_to_python(self, value, dsc=None): # pylint: disable=C0103
646586
"""
647587
Note that a SET is a string too, but using the FieldFlag we can see
@@ -670,15 +610,14 @@ def _STRING_to_python(self, value, dsc=None): # pylint: disable=C0103
670610
return value
671611

672612
_VAR_STRING_to_python = _STRING_to_python
613+
_JSON_to_python = _STRING_to_python
673614

674615
def _BLOB_to_python(self, value, dsc=None): # pylint: disable=C0103
675-
"""Convert BLOB data type to Python"""
676-
if not value:
677-
# This is an empty BLOB
678-
return ""
679-
# JSON Values are stored in LONG BLOB, but the blob values recived
680-
# from the server are not dilutable, check for JSON values.
681-
return self._JSON_to_python(value, dsc)
616+
"""Convert BLOB data type to Python."""
617+
if dsc is not None:
618+
if dsc[7] & FieldFlag.BLOB and dsc[7] & FieldFlag.BINARY:
619+
return bytes(value)
620+
return self._STRING_to_python(value, dsc)
682621

683622
_LONG_BLOB_to_python = _BLOB_to_python
684623
_MEDIUM_BLOB_to_python = _BLOB_to_python

src/mysql_capi.c

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2626,8 +2626,15 @@ MySQL_fetch_row(MySQL *self)
26262626
}
26272627
else if (field_type == MYSQL_TYPE_BLOB)
26282628
{
2629-
value= mytopy_string(row[i], field_lengths[i], field_flags,
2630-
charset, self->use_unicode);
2629+
if ((field_flags & BLOB_FLAG) && (field_flags & BINARY_FLAG))
2630+
{
2631+
value= BytesFromStringAndSize(row[i], field_lengths[i]);
2632+
}
2633+
else
2634+
{
2635+
value= mytopy_string(row[i], field_lengths[i], field_flags,
2636+
charset, self->use_unicode);
2637+
}
26312638
PyTuple_SET_ITEM(result_row, i, value);
26322639
}
26332640
else if (field_type == MYSQL_TYPE_GEOMETRY)

tests/test_bugs.py

Lines changed: 57 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -4717,6 +4717,7 @@ def test_automatically_set_of_unread_rows(self):
47174717

47184718
cur_cext.close()
47194719

4720+
47204721
@unittest.skipIf(tests.MYSQL_VERSION < (8, 0, 1),
47214722
"Collation utf8mb4_0900_ai_ci not available on 5.7.x")
47224723
class BugOra27277964(tests.MySQLConnectorTests):
@@ -4741,158 +4742,6 @@ def test_execute_utf8mb4_collation(self):
47414742
self.cur.execute("INSERT INTO {0} VALUES (1, 'Nuno')".format(self.tbl))
47424743

47434744

4744-
@unittest.skipIf(tests.MYSQL_VERSION < (5, 7, 8),
4745-
"Support for native JSON data types introduced on 5.7.8 ")
4746-
class BugOra24948186(tests.MySQLConnectorTests):
4747-
"""BUG#24948186: MySQL JSON TYPES RETURNED AS BYTES INSTEAD OF PYTHON TYPES
4748-
"""
4749-
def setUp(self):
4750-
pass
4751-
4752-
def tearDown(self):
4753-
pass
4754-
4755-
def run_test_mysql_json_type(self, stm, test_values, expected_values,
4756-
mysql_type, expected_type):
4757-
config = tests.get_mysql_config()
4758-
config['charset'] = "utf8"
4759-
config['use_unicode'] = True
4760-
cnx = connection.MySQLConnection(**config)
4761-
cur = cnx.cursor()
4762-
4763-
for test_value, expected_value in zip(test_values, expected_values):
4764-
cur.execute(stm.format(value=test_value))
4765-
row = cur.fetchall()[0]
4766-
self.assertEqual(row[0], expected_value) #,"value is not the expected")
4767-
self.assertTrue(isinstance(row[0], expected_type),
4768-
u"value {} is not python type {}"
4769-
u"".format(row[0], expected_type))
4770-
self.assertEqual(row[1], mysql_type,
4771-
u"value {} is mysql type {} but expected {}"
4772-
u"".format(row[0], row[1], mysql_type))
4773-
cur.close()
4774-
cnx.close()
4775-
4776-
@foreach_cnx()
4777-
def test_retrieve_mysql_json_boolean(self):
4778-
stm = ("SELECT j, JSON_TYPE(j)"
4779-
"from (SELECT CAST({value} AS JSON) as J) jdata")
4780-
test_values = ["true", "false"]
4781-
expected_values = [True, False]
4782-
mysql_type = "BOOLEAN"
4783-
expected_type = bool
4784-
self.run_test_mysql_json_type(stm, test_values, expected_values,
4785-
mysql_type, expected_type)
4786-
4787-
@foreach_cnx()
4788-
def test_retrieve_mysql_json_integer(self):
4789-
arch64bit = sys.maxsize > 2**32
4790-
stm = ("SELECT j->>'$.foo', JSON_TYPE(j->>'$.foo')"
4791-
"FROM (SELECT json_object('foo', {value}) AS j) jdata")
4792-
test_values = [-2147483648, -1, 0, 1, 2147483647]
4793-
expected_values = test_values
4794-
mysql_type = "INTEGER"
4795-
expected_type = int
4796-
self.run_test_mysql_json_type(stm, test_values, expected_values,
4797-
mysql_type, expected_type)
4798-
4799-
test_values = [-9223372036854775808]
4800-
expected_values = test_values
4801-
mysql_type = "INTEGER"
4802-
if PY2 and (os.name == "nt" or not arch64bit):
4803-
expected_type = long
4804-
else:
4805-
expected_type = int
4806-
self.run_test_mysql_json_type(stm, test_values, expected_values,
4807-
mysql_type, expected_type)
4808-
4809-
test_values = [92233720368547760]
4810-
expected_values = test_values
4811-
mysql_type = "UNSIGNED INTEGER"
4812-
if PY2 and (os.name == "nt" or not arch64bit):
4813-
expected_type = long
4814-
else:
4815-
expected_type = int
4816-
self.run_test_mysql_json_type(stm, test_values,
4817-
expected_values,
4818-
mysql_type, expected_type)
4819-
4820-
test_values = [18446744073709551615]
4821-
expected_values = test_values
4822-
mysql_type = "UNSIGNED INTEGER"
4823-
if PY2:
4824-
expected_type = long
4825-
else:
4826-
expected_type = int
4827-
self.run_test_mysql_json_type(stm, test_values,
4828-
expected_values,
4829-
mysql_type, expected_type)
4830-
4831-
@foreach_cnx()
4832-
def test_retrieve_mysql_json_double(self):
4833-
# Because floating-point values are approximate and not stored as exact
4834-
# values the test values are not the true maximun or minum values
4835-
stm = ("SELECT j->>'$.foo', JSON_TYPE(j->>'$.foo')"
4836-
"FROM (SELECT json_object('foo', {value}) AS j) jdata")
4837-
test_values = [-12345.555, -1.55, 1.55, 12345.555]
4838-
expected_values = test_values
4839-
mysql_type = "DOUBLE"
4840-
expected_type = float
4841-
self.run_test_mysql_json_type(stm, test_values, expected_values,
4842-
mysql_type, expected_type)
4843-
4844-
@foreach_cnx()
4845-
def test_retrieve_mysql_json_string(self):
4846-
stm = (u"SELECT j->>'$.foo', JSON_TYPE(j->>'$.foo')"
4847-
u"FROM (SELECT json_object('foo', {value}) AS j) jdata")
4848-
test_values = ['\'" "\'', '\'"some text"\'', u'\'"データベース"\'']
4849-
expected_values = ['" "', '"some text"', u'"データベース"']
4850-
mysql_type = "STRING"
4851-
expected_type = STRING_TYPES
4852-
self.run_test_mysql_json_type(stm, test_values, expected_values,
4853-
mysql_type, expected_type)
4854-
4855-
@foreach_cnx()
4856-
def test_retrieve_mysql_json_datetime_types(self):
4857-
stm = ("SELECT j, JSON_TYPE(j)"
4858-
"from (SELECT CAST({value} AS JSON) as J) jdata")
4859-
test_values = ["cast('1972-01-01 00:42:49.000000' as DATETIME)",
4860-
"cast('2018-01-01 23:59:59.000000' as DATETIME)"]
4861-
expected_values = [datetime(1972, 1, 1, 0, 42, 49),
4862-
datetime(2018, 1, 1, 23, 59, 59)]
4863-
mysql_type = "DATETIME"
4864-
expected_type = datetime
4865-
self.run_test_mysql_json_type(stm, test_values, expected_values,
4866-
mysql_type, expected_type)
4867-
4868-
@foreach_cnx()
4869-
def test_retrieve_mysql_json_date_types(self):
4870-
stm = ("SELECT j, JSON_TYPE(j)"
4871-
"from (SELECT CAST({value} AS JSON) as J) jdata")
4872-
test_values = ["DATE('1972-01-01')",
4873-
"DATE('2018-12-31')"]
4874-
expected_values = [date(1972, 1, 1),
4875-
date(2018, 12, 31)]
4876-
mysql_type = "DATE"
4877-
expected_type = date
4878-
self.run_test_mysql_json_type(stm, test_values, expected_values,
4879-
mysql_type, expected_type)
4880-
4881-
@foreach_cnx()
4882-
def test_retrieve_mysql_json_time_types(self):
4883-
stm = ("SELECT j, JSON_TYPE(j)"
4884-
"from (SELECT CAST({value} AS JSON) as J) jdata")
4885-
test_values = ["TIME('00:42:49.000000')",
4886-
"TIME('23:59:59.000001')"]
4887-
expected_values = [timedelta(hours=0, minutes=42, seconds=49),
4888-
timedelta(hours=23, minutes=59, seconds=59,
4889-
microseconds=1)]
4890-
mysql_type = "TIME"
4891-
expected_type = timedelta
4892-
self.run_test_mysql_json_type(stm, test_values, expected_values,
4893-
mysql_type, expected_type)
4894-
4895-
48964745
@unittest.skipIf(tests.MYSQL_VERSION < (8, 0, 11),
48974746
"Not support for TLSv1.2 or not available by default")
48984747
class Bug26484601(tests.MySQLConnectorTests):
@@ -5357,14 +5206,8 @@ def run_test_retrieve_stored_type(self, stm, test_values, expected_values,
53575206

53585207
rows = cnx.get_rows()[0][len(test_values) * (-1):]
53595208
for returned_val, expected_value in zip(rows, expected_values):
5360-
self.assertEqual(returned_val[0], expected_value,
5361-
u"value {} is not the expected {}."
5362-
u"".format(returned_val[0], expected_value))
5363-
self.assertTrue(isinstance(returned_val[0], expected_type),
5364-
u"value {} is not python type {},"
5365-
u"instead found a type {}"
5366-
u"".format(returned_val[0], expected_type,
5367-
type(returned_val[0])))
5209+
self.assertEqual(returned_val[0], expected_value)
5210+
self.assertTrue(isinstance(returned_val[0], expected_type))
53685211

53695212
cur.close()
53705213
cnx.close()
@@ -5401,8 +5244,11 @@ def test_retrieve_stored_blob(self):
54015244
stm = self.insert_stmt.format(self.table_name, column)
54025245
test_values = ['\' \'', '\'some text\'', u'\'データベース\'',
54035246
"\"'12345'\""]
5404-
expected_values = [' ', 'some text', u'データベース', "'12345'"]
5405-
expected_type = STRING_TYPES
5247+
expected_values = [b' ', b'some text', b'\xe3\x83\x87\xe3\x83\xbc\xe3'
5248+
b'\x82\xbf\xe3\x83\x99\xe3\x83\xbc\xe3\x82\xb9'
5249+
if PY2 else u'データベース'.encode("utf-8"),
5250+
b"'12345'"]
5251+
expected_type = bytes
54065252

54075253
self.run_test_retrieve_stored_type(stm, test_values, expected_values,
54085254
column, expected_type)
@@ -5613,7 +5459,7 @@ def test_retrieve_from_LONGBLOB(self):
56135459
# "12345" handle as datetime in JSON produced index error.
56145460
# LONGBLOB can store big data
56155461
test_values = ["", "12345", '"54321"', "A"*(2**20)]
5616-
expected_values = ["", 12345, '"54321"', "A"*(2**20)]
5462+
expected_values = [b"", b"12345", b'"54321"', b"A"*(2**20)]
56175463
stm = "INSERT INTO {} (col1, col2) VALUES ('{}', '{}')"
56185464

56195465
for num, test_value in zip(range(len(test_values)), test_values):
@@ -5705,6 +5551,7 @@ def test_read_default_file_alias(self):
57055551
mysql.connector._CONNECTION_POOLS = {}
57065552
conn.close()
57075553

5554+
57085555
@unittest.skipIf(tests.MYSQL_VERSION < (5, 7, 3),
57095556
"MySQL >= 5.7.3 is required for reset command")
57105557
@unittest.skipIf(CMySQLConnection is None,
@@ -5763,3 +5610,50 @@ def test_cext_pool_support(self):
57635610
self.assertNotEqual(('2',), cnx.get_rows()[0][0])
57645611
else:
57655612
self.assertNotEqual((b'2',), cnx.get_rows()[0][0])
5613+
5614+
5615+
@unittest.skipIf(tests.MYSQL_VERSION < (5, 7, 8), "No JSON support")
5616+
class BugOra29808262(tests.MySQLConnectorTests):
5617+
"""BUG#229808262: TEXT COLUMN WITH ONLY DIGITS READS IN AS INT.
5618+
"""
5619+
table_name = "BugOra29808262"
5620+
5621+
def setUp(self):
5622+
pass
5623+
5624+
def tearDown(self):
5625+
pass
5626+
5627+
@foreach_cnx()
5628+
def test_blob_fields(self):
5629+
cur = self.cnx.cursor()
5630+
cur.execute("DROP TABLE IF EXISTS {}".format(self.table_name))
5631+
cur.execute("CREATE TABLE {} ("
5632+
" my_blob BLOB,"
5633+
" my_longblob LONGBLOB,"
5634+
" my_json JSON,"
5635+
" my_text TEXT) CHARACTER SET utf8"
5636+
" COLLATE utf8_general_ci".format(self.table_name))
5637+
5638+
test_values = (
5639+
"BLOB" * (2**10),
5640+
"LONG_BLOB" * (2**20),
5641+
'{"lat": "41.14961", "lon": "-8.61099", "name": "Porto"}',
5642+
"My TEXT",
5643+
)
5644+
expected_values = (
5645+
b"BLOB" * (2**10),
5646+
b"LONG_BLOB" * (2**20),
5647+
'{"lat": "41.14961", "lon": "-8.61099", "name": "Porto"}',
5648+
"My TEXT",
5649+
)
5650+
cur = self.cnx.cursor()
5651+
cur.execute("INSERT INTO {} VALUES ('{}')"
5652+
"".format(self.table_name, "', '".join(test_values)))
5653+
cur.execute("SELECT my_blob, my_longblob, my_json, my_text FROM {}"
5654+
"".format(self.table_name))
5655+
res = cur.fetchall()
5656+
self.assertEqual(res[0], expected_values)
5657+
5658+
cur.execute("DROP TABLE IF EXISTS {}".format(self.table_name))
5659+
cur.close()

0 commit comments

Comments
 (0)