Skip to content

Commit 13f5cd5

Browse files
committed
BUG#37774513: Inconsistent conversion to_sql for cext vs pure python
This patch fixes the inconsistency of data type conversion support between pure-python and c-extension based connector. Change-Id: I829a2871e6dc2ebe782be74cbe01fd60c8c2be5b
1 parent 9b86b29 commit 13f5cd5

File tree

7 files changed

+458
-118
lines changed

7 files changed

+458
-118
lines changed

CHANGES.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ v9.4.0
1515
- WL#16963: Update the OpenTelemetry version
1616
- WL#16962: Update the Python Protobuf version
1717
- BUG#37868219: RPM packages have incorrect copyright year in their metadata
18-
- BUG#37820231: Text based django ORM filters doesn't work with Connector/Python
1918
- BUG#37859771: mysql/connector python version 9.3.0 has a regression which cannot persist binary data with percent signs in it
19+
- BUG#37820231: Text based django ORM filters doesn't work with Connector/Python
2020
- BUG#37806057: Rename extra option (when installing wheel package) to install webauthn functionality dependencies
21+
- BUG#37774513: Inconsistent conversion to_sql for cext vs pure python
2122
- BUG#37642447: The license type is missing from RPM package
2223
- BUG#37627508: mysql/connector python fetchmany() has an off by one bug when argument given as 1
2324
- BUG#37047789: Python connector does not support Django enum

mysql-connector-python/lib/mysql/connector/constants.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131
import warnings
3232

3333
from abc import ABC, ABCMeta
34+
from datetime import date, datetime, time, timedelta
35+
from decimal import Decimal
36+
from time import struct_time
3437
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union, ValuesView
3538

3639
from .charsets import MYSQL_CHARACTER_SETS, MYSQL_CHARACTER_SETS_57
@@ -284,6 +287,26 @@
284287
"TLSv1.3": TLSV1_3_CIPHER_SUITES.values(),
285288
}
286289

290+
NATIVE_SUPPORTED_CONVERSION_TYPES = {
291+
int: "int",
292+
float: "float",
293+
str: "str",
294+
bytes: "bytes",
295+
bytearray: "bytearray",
296+
bool: "bool",
297+
type(None): "nonetype",
298+
datetime: "datetime",
299+
date: "date",
300+
time: "time",
301+
struct_time: "struct_time",
302+
timedelta: "timedelta",
303+
Decimal: "decimal",
304+
}
305+
"""Dictionary of the supported data types of the default MySQLConverter class
306+
and the corresponding tag names used to map against their respective methods
307+
'_{tag_name}_to_mysql(...)'. These converter methods are then used to convert
308+
the matched data type to mysql recognizable type."""
309+
287310

288311
def flag_is_set(flag: int, flags: int) -> bool:
289312
"""Checks if the flag is set

mysql-connector-python/lib/mysql/connector/conversion.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040

4141
from .constants import (
4242
MYSQL_VECTOR_TYPE_CODE,
43+
NATIVE_SUPPORTED_CONVERSION_TYPES,
4344
CharacterSet,
4445
FieldFlag,
4546
FieldType,
@@ -236,13 +237,26 @@ def to_mysql(self, value: MySQLConvertibleType) -> MySQLProducedType:
236237
"""Convert Python data type to MySQL"""
237238
if isinstance(value, Enum):
238239
value = value.value
239-
type_name = value.__class__.__name__.lower()
240+
# check if type of value object matches any one of the native supported conversion types
241+
# most of the types will match the condition below
242+
type_name: str = NATIVE_SUPPORTED_CONVERSION_TYPES.get(type(value), "")
243+
if not type_name:
244+
# check if the value object inherits from one of the native supported conversion types
245+
type_name = next(
246+
(
247+
name
248+
for data_type, name in NATIVE_SUPPORTED_CONVERSION_TYPES.items()
249+
if isinstance(value, data_type)
250+
),
251+
value.__class__.__name__.lower(),
252+
)
240253
try:
241254
converted: MySQLProducedType = getattr(self, f"_{type_name}_to_mysql")(
242255
value
243256
)
244257
return converted
245258
except AttributeError:
259+
# Value type is not a native one, nor a subclass of a native one
246260
if self.str_fallback:
247261
return str(value).encode()
248262
raise TypeError(

mysql-connector-python/src/include/mysql_capi_conversion.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2014, 2024, Oracle and/or its affiliates.
2+
* Copyright (c) 2014, 2025, 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
@@ -33,6 +33,9 @@
3333

3434
#include <Python.h>
3535

36+
int
37+
is_decimal_instance(PyObject *obj);
38+
3639
PyObject *
3740
pytomy_date(PyObject *obj);
3841

mysql-connector-python/src/mysql_capi.c

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2014, 2024, Oracle and/or its affiliates.
2+
* Copyright (c) 2014, 2025, 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
@@ -2010,16 +2010,16 @@ MySQL_convert_to_mysql(MySQL *self, PyObject *args)
20102010
// datetime is handled first
20112011
new_value = pytomy_datetime(value);
20122012
}
2013-
else if (PyDate_CheckExact(value)) {
2013+
else if (PyDate_Check(value)) {
20142014
new_value = pytomy_date(value);
20152015
}
20162016
else if (PyTime_Check(value)) {
20172017
new_value = pytomy_time(value);
20182018
}
2019-
else if (PyDelta_CheckExact(value)) {
2019+
else if (PyDelta_Check(value)) {
20202020
new_value = pytomy_timedelta(value);
20212021
}
2022-
else if (strcmp((value)->ob_type->tp_name, "decimal.Decimal") == 0) {
2022+
else if (is_decimal_instance(value)) {
20232023
new_value = pytomy_decimal(value);
20242024
}
20252025
else if (self->converter_str_fallback == Py_True) {
@@ -2194,7 +2194,7 @@ MySQL_query(MySQL *self, PyObject *args, PyObject *kwds)
21942194
continue;
21952195
}
21962196
/* DATE */
2197-
else if (PyDate_CheckExact(value)) {
2197+
else if (PyDate_Check(value)) {
21982198
MYSQL_TIME *date = &pbind->buffer.t;
21992199
date->year = PyDateTime_GET_YEAR(value);
22002200
date->month = PyDateTime_GET_MONTH(value);
@@ -2225,7 +2225,7 @@ MySQL_query(MySQL *self, PyObject *args, PyObject *kwds)
22252225
continue;
22262226
}
22272227
/* datetime.timedelta is TIME */
2228-
else if (PyDelta_CheckExact(value)) {
2228+
else if (PyDelta_Check(value)) {
22292229
MYSQL_TIME *time = &pbind->buffer.t;
22302230
time->hour = PyDateTime_TIME_GET_HOUR(value);
22312231
time->minute = PyDateTime_TIME_GET_MINUTE(value);
@@ -2244,7 +2244,7 @@ MySQL_query(MySQL *self, PyObject *args, PyObject *kwds)
22442244
continue;
22452245
}
22462246
/* DECIMAL */
2247-
else if (strcmp((value)->ob_type->tp_name, "decimal.Decimal") == 0) {
2247+
else if (is_decimal_instance(value)) {
22482248
pbind->str_value = pytomy_decimal(value);
22492249
mbind[i].buffer_type = MYSQL_TYPE_DECIMAL;
22502250
}
@@ -3383,7 +3383,7 @@ MySQLPrepStmt_execute(MySQLPrepStmt *self, PyObject *args, PyObject *kwds)
33833383
continue;
33843384
}
33853385
/* DATE */
3386-
else if (PyDate_CheckExact(value)) {
3386+
else if (PyDate_Check(value)) {
33873387
MYSQL_TIME *date = &pbind->buffer.t;
33883388
date->year = PyDateTime_GET_YEAR(value);
33893389
date->month = PyDateTime_GET_MONTH(value);
@@ -3414,7 +3414,7 @@ MySQLPrepStmt_execute(MySQLPrepStmt *self, PyObject *args, PyObject *kwds)
34143414
continue;
34153415
}
34163416
/* datetime.timedelta is TIME */
3417-
else if (PyDelta_CheckExact(value)) {
3417+
else if (PyDelta_Check(value)) {
34183418
MYSQL_TIME *time = &pbind->buffer.t;
34193419
time->hour = PyDateTime_TIME_GET_HOUR(value);
34203420
time->minute = PyDateTime_TIME_GET_MINUTE(value);
@@ -3433,7 +3433,7 @@ MySQLPrepStmt_execute(MySQLPrepStmt *self, PyObject *args, PyObject *kwds)
34333433
continue;
34343434
}
34353435
/* DECIMAL */
3436-
else if (strcmp((value)->ob_type->tp_name, "decimal.Decimal") == 0) {
3436+
else if (is_decimal_instance(value)) {
34373437
pbind->str_value = pytomy_decimal(value);
34383438
mbind[i].buffer_type = MYSQL_TYPE_DECIMAL;
34393439
}

mysql-connector-python/src/mysql_capi_conversion.c

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2014, 2024, Oracle and/or its affiliates.
2+
* Copyright (c) 2014, 2025, 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
@@ -44,6 +44,34 @@
4444
#define MINYEAR 1
4545
#define MAXYEAR 9999
4646

47+
48+
// Decimal module needs to be loaded at runtime
49+
static PyObject *decimal_class = NULL;
50+
51+
/**
52+
Check whether the type of object is a decimal.
53+
54+
Check whether the object passed via parameter is an instance of decimal class.
55+
56+
@param obj the PyObject to be checked
57+
58+
@return 1 if obj is an instance of decimal class, 0 otherwise.
59+
@retval 1 Instance of the decimal class
60+
@retval 0 Not an instance of the decimal class
61+
*/
62+
int is_decimal_instance(PyObject *obj)
63+
{
64+
if (!decimal_class) {
65+
PyObject *decimal_module = PyImport_ImportModule("decimal");
66+
if (decimal_module) {
67+
decimal_class = PyObject_GetAttrString(decimal_module, "Decimal");
68+
}
69+
}
70+
return (decimal_class)
71+
? PyObject_IsInstance(obj, decimal_class)
72+
: strcmp((obj)->ob_type->tp_name, "decimal.Decimal") == 0;
73+
}
74+
4775
/**
4876
Check whether a year is a leap year.
4977

0 commit comments

Comments
 (0)