Ticket #19625: django19625-master.fix_using_django_conversions.patch

File django19625-master.fix_using_django_conversions.patch, 7.6 KB (added by wdoekes, 2 years ago)

Proper fix that seems to work. Includes tests.

  • django/db/backends/mysql/base.py

    diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py
    index f24df93..68b4f5f 100644
    a b Requires MySQLdb: http://sourceforge.net/projects/mysql-python 
    66from __future__ import unicode_literals
    77
    88import datetime
     9import decimal
    910import re
    1011import sys
    1112import warnings
    def adapt_datetime_with_timezone_support(value, conv): 
    7374        value = value.astimezone(timezone.utc).replace(tzinfo=None)
    7475    return Thing2Literal(value.strftime("%Y-%m-%d %H:%M:%S"), conv)
    7576
     77def rev_typecast_decimal(value, conv):
     78    # value is always an instance of decimal.Decimal
     79    if value.is_normal() or value.is_zero():
     80        return str(value)
     81    raise ValueError('Cannot express %r in a way mysql understands' % value)
     82
    7683# MySQLdb-1.2.1 returns TIME columns as timedelta -- they are more like
    7784# timedelta in terms of actual behavior as they are signed and include days --
    7885# and Django expects time, so we still need to override that. We also need to
    django_conversions.update({ 
    8794    FIELD_TYPE.NEWDECIMAL: util.typecast_decimal,
    8895    FIELD_TYPE.DATETIME: parse_datetime_with_timezone_support,
    8996    datetime.datetime: adapt_datetime_with_timezone_support,
     97    decimal.Decimal: rev_typecast_decimal,
    9098})
    9199
    92100# This should match the numerical portion of the version numbers (we can treat
  • new file tests/modeltests/long_decimal/models.py

    diff --git a/tests/modeltests/long_decimal/__init__.py b/tests/modeltests/long_decimal/__init__.py
    new file mode 100644
    index 0000000..e69de29
    diff --git a/tests/modeltests/long_decimal/models.py b/tests/modeltests/long_decimal/models.py
    new file mode 100644
    index 0000000..9a8772b
    - +  
     1# -*- coding: utf-8 -*-
     2"""
     312. Using a decimal field in lookups
     4
     5MySQL does not convert strings to Decimals that well. Make sure inserting
     6and filtering works properly.
     7"""
     8
     9from __future__ import absolute_import
     10
     11from django.db import models
     12
     13try:
     14    from django.utils.encoding import python_2_unicode_compatible
     15except ImportError:
     16    python_2_unicode_compatible = None
     17
     18
     19class Book(models.Model):
     20    isbn = models.DecimalField(max_digits=31, decimal_places=0, db_index=True)
     21    name = models.CharField(max_length=63)
     22
     23    def __str__(self):
     24        return "%s %s" % (self.isbn, self.name)
     25
     26
     27if python_2_unicode_compatible:
     28    Book = python_2_unicode_compatible(Book)
  • new file tests/modeltests/long_decimal/tests.py

    diff --git a/tests/modeltests/long_decimal/tests.py b/tests/modeltests/long_decimal/tests.py
    new file mode 100644
    index 0000000..97b452c
    - +  
     1# vim: set ts=8 sw=4 sts=4 et ai:
     2from __future__ import absolute_import, unicode_literals
     3
     4from decimal import Decimal
     5
     6from django.test import TestCase
     7
     8from .models import Book
     9
     10"""
     11Note that this problem is fixed in MySQL somewhere between
     12version 5.1.66 and 5.5.28.
     13
     14Test with:
     15
     16create table abc (value decimal(31,0));
     17insert into abc values (1234567890123456789012345678901);
     18select * from abc where value = 1234567890123456789012345678901;
     19select * from abc where value = '1234567890123456789012345678901';
     20drop table abc;
     21
     22In the broken case, you won't get a result for the second query.
     23
     24The fix is to have Django always send decimals to the SQL backend as-is
     25and not as a quoted string.
     26"""
     27
     28class DecimalTests(TestCase):
     29    def help_decimal_field(self, elements, iterable):
     30        try:
     31            self.help_decimal_field_setup(elements, iterable)
     32            self.help_decimal_field_test(elements, iterable)
     33        finally:
     34            Book.objects.all().delete()
     35
     36    def help_decimal_field_values(self, elements, size):
     37        value = ''.join([elements[(i % len(elements))] for i in range(size)])
     38        isbn = Decimal(value)
     39        name = 'ISBN:%s' % (value,)
     40        return isbn, name
     41
     42    def help_decimal_field_setup(self, elements, iterable):
     43        for size in iterable:
     44            isbn, name = self.help_decimal_field_values(elements, size)
     45            Book.objects.create(isbn=isbn, name=name)
     46
     47    def help_decimal_field_test(self, elements, iterable):
     48        for size in iterable:
     49            isbn, name = self.help_decimal_field_values(elements, size)
     50            try:
     51                book = Book.objects.get(isbn=isbn)
     52            except Book.DoesNotExist:
     53                self.assertFalse(True, 'book with isbn %s was not found' % (
     54                                 isbn,))
     55            except:
     56                # Certain fixes can produce an unexpected MySQL Warning..
     57                import sys
     58                print >>sys.stderr, '( failure when getting', isbn, ')'
     59                raise
     60            else:
     61                self.assertEqual(book.isbn, isbn)
     62                self.assertEqual(book.name, name)
     63
     64    def test_baseline(self):
     65        """
     66        This should always work. The point was that larger decimals get
     67        cast to a lossy float. For small values, there is no loss.
     68        """
     69        try:
     70            Book.objects.create(isbn=Decimal('987'), name='baseline')
     71            baseline = Book.objects.get(isbn=Decimal('987'))
     72            self.assertEquals(baseline.name, 'baseline')
     73        finally:
     74            Book.objects.all().delete()
     75
     76    def test_non_normal(self):
     77        # get_db_prep_save throws exceptions on nonnormal decimals
     78        # but select queries only pass them to the conversion mapping
     79        self.assertRaises(ValueError, Book.objects.get, isbn=Decimal('Infinity'))
     80        self.assertRaises(ValueError, Book.objects.get, isbn=Decimal('-Infinity'))
     81        self.assertRaises(ValueError, Book.objects.get, isbn=Decimal('NaN'))
     82        # zero is a non normal decimal
     83        try:
     84            Book.objects.create(isbn=Decimal('0'), name='zero')
     85            book = Book.objects.get(isbn=Decimal('0'))
     86            self.assertEquals(book.name, 'zero')
     87            book = Book.objects.get(isbn=Decimal('-0'))
     88            self.assertEquals(book.name, 'zero')
     89
     90            Book.objects.create(isbn=Decimal('1.0E+3'), name='1k')
     91            book = Book.objects.get(isbn=Decimal('1.0E+3'))
     92            self.assertEquals(book.name, '1k')
     93            book = Book.objects.get(isbn=Decimal('1000'))
     94            self.assertEquals(book.name, '1k')
     95        finally:
     96            Book.objects.all().delete()
     97
     98    def test_decimal_field_works1(self):
     99        """
     100        For some reason, the test doesn't fail when we iterate with this
     101        test from 1 to 31.
     102        """
     103        self.help_decimal_field('1234567890', range(1, 32))
     104
     105    def test_decimal_field_works2(self):
     106        """
     107        The original test reversed. Works too.
     108        """
     109        self.help_decimal_field('1234567890', range(31, 0, -1))
     110
     111    def test_decimal_field_broken1(self):
     112        """
     113        Testing with just one element. This fails nicely.
     114
     115        This fails on:
     116        * MySQL 5.1.66
     117
     118        Doesn't fail on:
     119        * MySQL 5.5.28
     120        """
     121        self.help_decimal_field('1234567890', [31])
     122
     123    def test_decimal_field_broken2(self):
     124        """
     125        Testing with just one element. This fails nicely.
     126
     127        This fails on:
     128        * MySQL 5.1.66
     129
     130        Doesn't fail on:
     131        * MySQL 5.5.28
     132        """
     133        self.help_decimal_field('1234567890', [22])
     134
     135    def test_decimal_field_broken3(self):
     136        """
     137        Testing with just one element. This fails nicely.
     138
     139        This fails on:
     140        * MySQL 5.1.66
     141
     142        Doesn't fail on:
     143        * MySQL 5.5.28
     144        """
     145        self.help_decimal_field('1234567890', [18])
Back to Top