Skip to content

Commit 2a2a52d

Browse files
committed
BUG#28821983: Fix rounding errors for decimal values
This patch fixes potential rounding errors with arithmetic expressions, in decimal data types. This issue was fixed by not quoting decimal values. Tests added for regression. Change-Id: Iba31f0f216242f8338fab39d23fbc53ad056e2be
1 parent 2fa32c5 commit 2a2a52d

File tree

5 files changed

+64
-10
lines changed

5 files changed

+64
-10
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ v8.0.30
1616
- WL#15035: Enforce PEP 7 and PEP 8 coding style
1717
- WL#14822: Refactor the authentication plugin mechanism
1818
- WL#14815: Support OpenSSL 3.0
19+
- BUG#28821983: Fix rounding errors for decimal values
1920

2021
v8.0.29
2122
=======

lib/mysql/connector/cursor.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import weakref
3333

3434
from collections import namedtuple
35+
from decimal import Decimal
3536

3637
from .abstracts import NAMED_TUPLE_CACHE, MySQLCursorAbstract
3738
from .constants import ServerFlag
@@ -396,11 +397,12 @@ def _process_params_dict(self, params):
396397
escape = self._connection.converter.escape
397398
quote = self._connection.converter.quote
398399
res = {}
399-
for key, value in list(params.items()):
400+
for key, value in params.items():
400401
conv = value
401402
conv = to_mysql(conv)
402403
conv = escape(conv)
403-
conv = quote(conv)
404+
if not isinstance(value, Decimal):
405+
conv = quote(conv)
404406
res[key.encode()] = conv
405407
except Exception as err:
406408
raise ProgrammingError(
@@ -412,15 +414,18 @@ def _process_params_dict(self, params):
412414
def _process_params(self, params):
413415
"""Process query parameters."""
414416
try:
415-
res = params
417+
res = params[:]
416418

417419
to_mysql = self._connection.converter.to_mysql
418420
escape = self._connection.converter.escape
419421
quote = self._connection.converter.quote
420422

421-
res = [to_mysql(i) for i in res]
422-
res = [escape(i) for i in res]
423-
res = [quote(i) for i in res]
423+
res = [to_mysql(value) for value in res]
424+
res = [escape(value) for value in res]
425+
res = [
426+
quote(value) if not isinstance(params[i], Decimal) else value
427+
for i, value in enumerate(res)
428+
]
424429
except Exception as err:
425430
raise ProgrammingError(
426431
f"Failed processing format-parameters; {err}"
@@ -607,8 +612,6 @@ def remove_comments(match):
607612
raise ProgrammingError(
608613
"Not all parameters were used in the SQL statement"
609614
)
610-
# for p in self._process_params(params):
611-
# tmp = tmp.replace(b'%s',p,1)
612615
values.append(tmp)
613616
if fmt in stmt:
614617
stmt = stmt.replace(fmt, b",".join(values), 1)

src/mysql_capi.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1922,6 +1922,10 @@ MySQL_convert_to_mysql(MySQL *self, PyObject *args)
19221922
if (new_value == Py_None) {
19231923
PyTuple_SET_ITEM(prepared, i, PyBytes_FromString("NULL"));
19241924
}
1925+
else if (strcmp((value)->ob_type->tp_name, "decimal.Decimal") == 0) {
1926+
Py_INCREF(new_value);
1927+
PyTuple_SET_ITEM(prepared, i, new_value);
1928+
}
19251929
else if (PyBytes_Check(new_value)) {
19261930
PyObject *quoted = PyBytes_FromFormat("'%s'", PyBytes_AsString(new_value));
19271931
PyTuple_SET_ITEM(prepared, i, quoted);

tests/test_bugs.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6867,3 +6867,49 @@ def test_expression_as_column_without_alias(self):
68676867
"""
68686868
)
68696869
_ = cur.fetchall()
6870+
6871+
6872+
class BugOra328821983(tests.MySQLConnectorTests):
6873+
"""BUG#328821983: Fix rounding errors when using decimal.Decimal."""
6874+
6875+
@foreach_cnx()
6876+
def test_decimal_update(self):
6877+
table = "BugOra328821983"
6878+
with self.cnx.cursor() as cur:
6879+
cur.execute(f"DROP TABLE IF EXISTS {table}")
6880+
cur.execute(
6881+
f"""
6882+
CREATE TABLE {table} (
6883+
id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
6884+
value DECIMAL(32,8) NOT NULL
6885+
)
6886+
"""
6887+
)
6888+
cur.execute(
6889+
f"INSERT INTO {table} VALUES (NULL, %s)",
6890+
(Decimal("100000000000.00000001"),),
6891+
)
6892+
cur.execute(f"SELECT value FROM {table}")
6893+
res = cur.fetchall()[0][0]
6894+
self.assertEqual(res, Decimal("100000000000.00000001"))
6895+
6896+
# Use this value to increment the decimal field
6897+
value = Decimal("0.00000101")
6898+
6899+
# Test update with tuple as placeholders
6900+
query = f"UPDATE {table} SET value=(value + %s) WHERE id=%s"
6901+
cur.execute(query, (value, 1))
6902+
6903+
cur.execute(f"SELECT value FROM {table}")
6904+
res = cur.fetchall()[0][0]
6905+
self.assertEqual(res, Decimal("100000000000.00000102"))
6906+
6907+
# Test update with dictionary as placeholders
6908+
query = f"UPDATE {table} SET value=(value + %(value)s) WHERE id=%(id)s"
6909+
cur.execute(query, {"value": value, "id": 1})
6910+
6911+
cur.execute(f"SELECT value FROM {table}")
6912+
res = cur.fetchall()[0][0]
6913+
self.assertEqual(res, Decimal("100000000000.00000203"))
6914+
6915+
cur.execute(f"DROP TABLE IF EXISTS {table}")

tests/test_cursor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ def test__process_params(self):
391391
b"128",
392392
b"1281288",
393393
b"3.14",
394-
b"'3.14'",
394+
b"3.14",
395395
b"'back\\\\slash'",
396396
b"'newline\\n'",
397397
b"'return\\r'",
@@ -456,7 +456,7 @@ def test__process_params_dict(self):
456456
b"b": b"128",
457457
b"c": b"1281288",
458458
b"d": b"3.14",
459-
b"e": b"'3.14'",
459+
b"e": b"3.14",
460460
b"f": b"'back\\\\slash'",
461461
b"g": b"'newline\\n'",
462462
b"h": b"'return\\r'",

0 commit comments

Comments
 (0)