Skip to content

Commit 88433e1

Browse files
committed
WL14689: Fallback conversion to str for types incompatible with MySQL
Currently, if one tries to insert a value of some type that is not compatible with the default Connector/Python converter, an error is raised. This patch introduces a new connection option 'converter_str_fallback', that allows the conversion to str of values types not supported by the Connector/Python default converter class, if set to True. In addition, this patch enables the usage of a custom class when the C extension is used.
1 parent 7343aaa commit 88433e1

File tree

12 files changed

+201
-30
lines changed

12 files changed

+201
-30
lines changed

lib/mysql/connector/abstracts.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ def __init__(self, **kwargs):
123123
self._pool_config_version = None
124124
self.converter = None
125125
self._converter_class = None
126+
self._converter_str_fallback = False
126127
self._compress = False
127128

128129
self._consume_results = False
@@ -1173,6 +1174,7 @@ def set_converter_class(self, convclass):
11731174
charset_name = CharacterSet.get_info(self._charset_id)[0]
11741175
self._converter_class = convclass
11751176
self.converter = convclass(charset_name, self._use_unicode)
1177+
self.converter.str_fallback = self._converter_str_fallback
11761178
else:
11771179
raise TypeError("Converter class should be a subclass "
11781180
"of conversion.MySQLConverterBase.")

lib/mysql/connector/connection.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1368,11 +1368,13 @@ def cmd_stmt_execute(self, statement_id, data=(), parameters=(), flags=0):
13681368
if self._client_flags & ClientFlag.CLIENT_QUERY_ATTRIBUTES:
13691369
execute_packet = self._protocol.make_stmt_execute(
13701370
statement_id, data, tuple(parameters), flags,
1371-
long_data_used, self.charset, self._query_attrs)
1371+
long_data_used, self.charset, self._query_attrs,
1372+
self._converter_str_fallback)
13721373
else:
13731374
execute_packet = self._protocol.make_stmt_execute(
13741375
statement_id, data, tuple(parameters), flags,
1375-
long_data_used, self.charset)
1376+
long_data_used, self.charset,
1377+
converter_str_fallback=self._converter_str_fallback)
13761378
packet = self._send_cmd(ServerCmd.STMT_EXECUTE, packet=execute_packet)
13771379
result = self._handle_binary_result(packet)
13781380
return result

lib/mysql/connector/connection_cext.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,9 @@ def _open_connection(self):
237237

238238
try:
239239
self._cmysql.connect(**cnx_kwargs)
240+
self._cmysql.converter_str_fallback = self._converter_str_fallback
241+
if self.converter:
242+
self.converter.str_fallback = self._converter_str_fallback
240243
except MySQLInterfaceError as exc:
241244
raise errors.get_mysql_exception(msg=exc.msg, errno=exc.errno,
242245
sqlstate=exc.sqlstate)
@@ -437,6 +440,7 @@ def fetch_eof_columns(self, prep_stmt=None):
437440
None,
438441
None,
439442
None,
443+
None,
440444
~int(col[9]) & FieldFlag.NOT_NULL,
441445
int(col[9])
442446
))
@@ -468,7 +472,9 @@ def cmd_stmt_prepare(self, statement):
468472
raise errors.OperationalError("MySQL Connection not available")
469473

470474
try:
471-
return self._cmysql.stmt_prepare(statement)
475+
stmt = self._cmysql.stmt_prepare(statement)
476+
stmt.converter_str_fallback = self._converter_str_fallback
477+
return stmt
472478
except MySQLInterfaceError as err:
473479
raise errors.InterfaceError(str(err))
474480

@@ -650,11 +656,28 @@ def prepare_for_mysql(self, params):
650656
Returns dict.
651657
"""
652658
if isinstance(params, (list, tuple)):
653-
result = self._cmysql.convert_to_mysql(*params)
659+
if self.converter:
660+
result = [
661+
self.converter.quote(
662+
self.converter.escape(
663+
self.converter.to_mysql(value)
664+
)
665+
) for value in params
666+
]
667+
else:
668+
result = self._cmysql.convert_to_mysql(*params)
654669
elif isinstance(params, dict):
655670
result = {}
656-
for key, value in params.items():
657-
result[key] = self._cmysql.convert_to_mysql(value)[0]
671+
if self.converter:
672+
for key, value in params.items():
673+
result[key] = self.converter.quote(
674+
self.converter.escape(
675+
self.converter.to_mysql(value)
676+
)
677+
)
678+
else:
679+
for key, value in params.items():
680+
result[key] = self._cmysql.convert_to_mysql(value)[0]
658681
else:
659682
raise ValueError("Could not process parameters")
660683

lib/mysql/connector/constants.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2009, 2020, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2009, 2021, 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
@@ -53,6 +53,7 @@
5353
'charset': 'utf8mb4',
5454
'collation': None,
5555
'converter_class': None,
56+
'converter_str_fallback': False,
5657
'autocommit': False,
5758
'time_zone': None,
5859
'sql_mode': None,

lib/mysql/connector/conversion.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,15 @@ class MySQLConverterBase(object):
4848
be a subclass of this class.
4949
"""
5050

51-
def __init__(self, charset='utf8', use_unicode=True):
51+
def __init__(self, charset='utf8', use_unicode=True, str_fallback=False):
5252
self.python_types = None
5353
self.mysql_types = None
5454
self.charset = None
5555
self.charset_id = 0
5656
self.use_unicode = None
5757
self.set_charset(charset)
58-
self.set_unicode(use_unicode)
58+
self.use_unicode = use_unicode
59+
self.str_fallback = str_fallback
5960
self._cache_field_types = {}
6061

6162
def set_charset(self, charset):
@@ -126,8 +127,8 @@ class MySQLConverter(MySQLConverterBase):
126127
127128
"""
128129

129-
def __init__(self, charset=None, use_unicode=True):
130-
MySQLConverterBase.__init__(self, charset, use_unicode)
130+
def __init__(self, charset=None, use_unicode=True, str_fallback=False):
131+
MySQLConverterBase.__init__(self, charset, use_unicode, str_fallback)
131132
self._cache_field_types = {}
132133

133134
def escape(self, value):
@@ -179,6 +180,8 @@ def to_mysql(self, value):
179180
try:
180181
return getattr(self, "_{0}_to_mysql".format(type_name))(value)
181182
except AttributeError:
183+
if self.str_fallback:
184+
return str(value).encode()
182185
raise TypeError("Python '{0}' cannot be converted to a "
183186
"MySQL type".format(type_name))
184187

lib/mysql/connector/protocol.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2009, 2020, Oracle and/or its affiliates.
1+
# Copyright (c) 2009, 2021, Oracle and/or its affiliates.
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
@@ -660,7 +660,7 @@ def _prepare_stmt_send_long_data(self, statement, param, data):
660660

661661
def make_stmt_execute(self, statement_id, data=(), parameters=(),
662662
flags=0, long_data_used=None, charset='utf8',
663-
query_attrs=None):
663+
query_attrs=None, converter_str_fallback=False):
664664
"""Make a MySQL packet with the Statement Execute command"""
665665
iteration_count = 1
666666
null_bitmap = [0] * ((len(data) + 7) // 8)
@@ -725,6 +725,10 @@ def make_stmt_execute(self, statement_id, data=(), parameters=(),
725725
elif isinstance(value, (datetime.timedelta, datetime.time)):
726726
(packed, field_type) = self._prepare_binary_time(value)
727727
values.append(packed)
728+
elif converter_str_fallback:
729+
value = str(value).encode(charset)
730+
values.append(utils.lc_int(len(value)) + value)
731+
field_type = FieldType.STRING
728732
else:
729733
raise errors.ProgrammingError(
730734
"MySQL binary protocol can not handle "

src/include/mysql_capi.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2014, 2020, Oracle and/or its affiliates.
2+
* Copyright (c) 2014, 2021, Oracle and/or its affiliates.
33
*
44
* This program is free software; you can redistribute it and/or modify
55
* it under the terms of the GNU General Public License, version 2.0, as
@@ -61,6 +61,7 @@ typedef struct {
6161
PyObject *fields;
6262
PyObject *auth_plugin;
6363
PyObject *plugin_dir;
64+
PyObject *converter_str_fallback;
6465
MY_CHARSET_INFO cs;
6566
unsigned int connection_timeout;
6667
// class members
@@ -276,6 +277,7 @@ typedef struct {
276277
struct column_info *cols;
277278
PyObject *have_result_set;
278279
PyObject *fields;
280+
PyObject *converter_str_fallback;
279281
MY_CHARSET_INFO cs;
280282
} MySQLPrepStmt;
281283

src/mysql_capi.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,7 @@ MySQL_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
342342
self->use_unicode= 1;
343343
self->auth_plugin= PyUnicode_FromString("mysql_native_password");
344344
self->plugin_dir= PyUnicode_FromString(".");
345+
self->converter_str_fallback = Py_False;
345346

346347
return (PyObject *)self;
347348
}
@@ -1885,6 +1886,14 @@ MySQL_convert_to_mysql(MySQL *self, PyObject *args)
18851886
{
18861887
new_value= pytomy_decimal(value);
18871888
}
1889+
else if (self->converter_str_fallback == Py_True)
1890+
{
1891+
PyObject *str= PyObject_Str(value);
1892+
new_value= PyBytes_FromString(
1893+
(const char *)PyUnicode_1BYTE_DATA(str)
1894+
);
1895+
Py_DECREF(str);
1896+
}
18881897
else
18891898
{
18901899
PyOS_snprintf(error, 100,
@@ -3332,6 +3341,15 @@ MySQLPrepStmt_execute(MySQLPrepStmt *self, PyObject *args)
33323341
pbind->str_value= pytomy_decimal(value);
33333342
mbind[i].buffer_type= MYSQL_TYPE_DECIMAL;
33343343
}
3344+
else if (self->converter_str_fallback == Py_True)
3345+
{
3346+
PyObject *str= PyObject_Str(value);
3347+
pbind->str_value= PyBytes_FromString(
3348+
(const char *)PyUnicode_1BYTE_DATA(str)
3349+
);
3350+
mbind->buffer_type= MYSQL_TYPE_STRING;
3351+
Py_DECREF(str);
3352+
}
33353353
else
33363354
{
33373355
retval= PyErr_Format(MySQLInterfaceError,

src/mysql_connector.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2014, 2020, Oracle and/or its affiliates.
2+
* Copyright (c) 2014, 2021, Oracle and/or its affiliates.
33
*
44
* This program is free software; you can redistribute it and/or modify
55
* it under the terms of the GNU General Public License, version 2.0, as
@@ -71,6 +71,8 @@ static PyMemberDef MySQL_members[]=
7171
{
7272
{"have_result_set", T_OBJECT, offsetof(MySQL, have_result_set), 0,
7373
"True if current session has result set"},
74+
{"converter_str_fallback", T_OBJECT, offsetof(MySQL, converter_str_fallback), 0,
75+
"True for fallback to converting unsupported types to str"},
7476
{NULL} /* Sentinel */
7577
};
7678

@@ -306,6 +308,8 @@ static PyMemberDef MySQLPrepStmt_members[]=
306308
"True if statement has result set"},
307309
{"param_count", T_LONG, offsetof(MySQLPrepStmt, param_count), 1,
308310
"Returns the number of parameter markers present in the prepared statement"},
311+
{"converter_str_fallback", T_OBJECT, offsetof(MySQLPrepStmt, converter_str_fallback), 0,
312+
"True for fallback to converting unsupported types to str"},
309313
{NULL} /* Sentinel */
310314
};
311315

tests/cext/test_cext_connection.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
22

3-
# Copyright (c) 2014, 2020, Oracle and/or its affiliates.
3+
# Copyright (c) 2014, 2021, Oracle and/or its affiliates.
44
#
55
# This program is free software; you can redistribute it and/or modify
66
# it under the terms of the GNU General Public License, version 2.0, as
@@ -94,13 +94,13 @@ def test_cmd_query(self):
9494
exp = {
9595
'eof': {'status_flag': 32, 'warning_count': 0},
9696
'columns': [
97-
['Variable_name', 253, None, None, None, None, 0, 1],
98-
('Value', 253, None, None, None, None, 1, 0)
97+
['Variable_name', 253, None, None, None, None, None, 0, 1],
98+
('Value', 253, None, None, None, None, None, 1, 0)
9999
]
100100
}
101101

102102
if tests.MYSQL_VERSION >= (5, 7, 10):
103-
exp['columns'][0][7] = 4097
103+
exp['columns'][0][8] = 4097
104104
exp['eof']['status_flag'] = 16385
105105

106106
exp['columns'][0] = tuple(exp['columns'][0])

tests/test_connection.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,18 +1062,14 @@ def test_config(self):
10621062

10631063
# Test converter class
10641064
class TestConverter(MySQLConverterBase):
1065-
1066-
def __init__(self, charset=None, unicode=True):
1067-
pass
1065+
...
10681066

10691067
self.cnx.config(converter_class=TestConverter)
10701068
self.assertTrue(isinstance(self.cnx.converter, TestConverter))
10711069
self.assertEqual(self.cnx._converter_class, TestConverter)
10721070

10731071
class TestConverterWrong(object):
1074-
1075-
def __init__(self, charset, unicode):
1076-
pass
1072+
...
10771073

10781074
self.assertRaises(AttributeError,
10791075
self.cnx.config, converter_class=TestConverterWrong)
@@ -1316,15 +1312,13 @@ def test_set_converter_class(self):
13161312
"""Set the converter class"""
13171313

13181314
class TestConverterWrong(object):
1319-
def __init__(self, charset, unicode):
1320-
pass
1315+
...
13211316

13221317
self.assertRaises(TypeError,
13231318
self.cnx.set_converter_class, TestConverterWrong)
13241319

13251320
class TestConverter(MySQLConverterBase):
1326-
def __init__(self, charset, unicode):
1327-
pass
1321+
...
13281322

13291323
self.cnx.set_converter_class(TestConverter)
13301324
self.assertTrue(isinstance(self.cnx.converter, TestConverter))

0 commit comments

Comments
 (0)