Description: Fixed queries that may return unexpected results on MySQL due to typecasting.
Origin: backport, https://github.com/django/django/commit/aa80f498de6d687e613860933ac58433ab71ea4b
Forwarded: not-needed
Author: Salvatore Bonaccorso <carnil@debian.org>
Last-Update: 2014-05-11

Index: python-django/tests/regressiontests/model_fields/tests.py
===================================================================
--- python-django.orig/tests/regressiontests/model_fields/tests.py
+++ python-django/tests/regressiontests/model_fields/tests.py
@@ -6,7 +6,14 @@ import django.test
 from django import forms
 from django.db import models
 from django.core.exceptions import ValidationError
-
+from django.db.models.fields import (
+        AutoField, BigIntegerField, BooleanField, CharField,
+        CommaSeparatedIntegerField, DateField, DateTimeField, DecimalField,
+        EmailField, FilePathField, FloatField, IntegerField, IPAddressField,
+        NullBooleanField, PositiveIntegerField,
+        PositiveSmallIntegerField, SlugField, SmallIntegerField, TextField,
+        TimeField, URLField)
+from django.db.models.fields.files import FileField, ImageField, FieldFile
 from models import Foo, Bar, Whiz, BigD, BigS, Image, BigInt, Post, NullBooleanModel, BooleanModel
 
 # If PIL available, do these tests.
@@ -311,3 +318,83 @@ class TypeCoercionTests(django.test.Test
     def test_lookup_integer_in_textfield(self):
         self.assertEquals(Post.objects.filter(body=24).count(), 0)
 
+class PrepValueTest(django.test.TestCase):
+    def test_AutoField(self):
+        self.assertEqual(isinstance(AutoField(primary_key=True).get_prep_value(1), int), True)
+
+    def test_BigIntegerField(self):
+        self.assertEqual(isinstance(BigIntegerField().get_prep_value(long(9999999999999999999)), long), True)
+
+    def test_BooleanField(self):
+        self.assertEqual(isinstance(BooleanField().get_prep_value(True), bool), True)
+
+    def test_CharField(self):
+        self.assertEqual(isinstance(CharField().get_prep_value(''), str), True)
+        self.assertEqual(isinstance(CharField().get_prep_value(0), unicode), True)
+
+    def test_CommaSeparatedIntegerField(self):
+        self.assertEqual(isinstance(CommaSeparatedIntegerField().get_prep_value('1,2'), str), True)
+        self.assertEqual(isinstance(CommaSeparatedIntegerField().get_prep_value(0), unicode), True)
+
+    def test_DateField(self):
+        self.assertEqual(isinstance(DateField().get_prep_value(datetime.date.today()), datetime.date), True)
+
+    def test_DateTimeField(self):
+        self.assertEqual(isinstance(DateTimeField().get_prep_value(datetime.datetime.now()), datetime.datetime), True)
+
+    def test_DecimalField(self):
+        self.assertEqual(isinstance(DecimalField().get_prep_value(Decimal('1.2')), Decimal), True)
+
+    def test_EmailField(self):
+        self.assertEqual(isinstance(EmailField().get_prep_value('mailbox@domain.com'), str), True)
+
+    def test_FileField(self):
+        self.assertEqual(isinstance(FileField().get_prep_value('filename.ext'), unicode), True)
+        self.assertEqual(isinstance(FileField().get_prep_value(0), unicode), True)
+
+    def test_FilePathField(self):
+        self.assertEqual(isinstance(FilePathField().get_prep_value('tests.py'), unicode), True)
+        self.assertEqual(isinstance(FilePathField().get_prep_value(0), unicode), True)
+
+    def test_FloatField(self):
+        self.assertEqual(isinstance(FloatField().get_prep_value(1.2), float), True)
+
+    def test_ImageField(self):
+        self.assertEqual(isinstance(ImageField().get_prep_value('filename.ext'), unicode), True)
+
+    def test_IntegerField(self):
+        self.assertEqual(isinstance(IntegerField().get_prep_value(1), int), True)
+
+    def test_IPAddressField(self):
+        self.assertEqual(isinstance(IPAddressField().get_prep_value('127.0.0.1'), unicode), True)
+        self.assertEqual(isinstance(IPAddressField().get_prep_value(0), unicode), True)
+
+    def test_NullBooleanField(self):
+        self.assertEqual(isinstance(NullBooleanField().get_prep_value(True), bool), True)
+
+    def test_PositiveIntegerField(self):
+        self.assertEqual(isinstance(PositiveIntegerField().get_prep_value(1), int), True)
+
+    def test_PositiveSmallIntegerField(self):
+        self.assertEqual(isinstance(PositiveSmallIntegerField().get_prep_value(1), int), True)
+
+    def test_SlugField(self):
+        self.assertEqual(isinstance(SlugField().get_prep_value('slug'), str), True)
+        self.assertEqual(isinstance(SlugField().get_prep_value(0), unicode), True)
+
+    def test_SmallIntegerField(self):
+        self.assertEqual(isinstance(SmallIntegerField().get_prep_value(1), int), True)
+
+    def test_TextField(self):
+        self.assertEqual(isinstance(TextField().get_prep_value('Abc'), str), True)
+        self.assertEqual(isinstance(TextField().get_prep_value(0), unicode), True)
+
+    def test_TimeField(self):
+        self.assertEqual(isinstance(
+            TimeField().get_prep_value(datetime.datetime.now().time()),
+            datetime.time),
+            True)
+
+    def test_URLField(self):
+        self.assertEqual(isinstance(URLField().get_prep_value('http://domain.com'), str), True)
+
Index: python-django/django/db/models/fields/__init__.py
===================================================================
--- python-django.orig/django/db/models/fields/__init__.py
+++ python-django/django/db/models/fields/__init__.py
@@ -814,6 +814,12 @@ class FilePathField(Field):
         kwargs['max_length'] = kwargs.get('max_length', 100)
         Field.__init__(self, verbose_name, name, **kwargs)
 
+    def get_prep_value(self, value):
+        value = super(FilePathField, self).get_prep_value(value)
+        if value is None:
+            return None
+        return smart_unicode(value)
+
     def formfield(self, **kwargs):
         defaults = {
             'path': self.path,
@@ -909,6 +915,12 @@ class IPAddressField(Field):
         kwargs['max_length'] = 15
         Field.__init__(self, *args, **kwargs)
 
+    def get_prep_value(self, value):
+        value = super(IPAddressField, self).get_prep_value(value)
+        if value is None:
+            return None
+        return smart_unicode(value)
+
     def get_internal_type(self):
         return "IPAddressField"
 
Index: python-django/docs/howto/custom-model-fields.txt
===================================================================
--- python-django.orig/docs/howto/custom-model-fields.txt
+++ python-django/docs/howto/custom-model-fields.txt
@@ -472,6 +472,16 @@ For example::
             return ''.join([''.join(l) for l in (value.north,
                     value.east, value.south, value.west)])
 
+.. warning::
+
+    If your custom field uses the ``CHAR``, ``VARCHAR`` or ``TEXT``
+    types for MySQL, you must make sure that :meth:`.get_prep_value`
+    always returns a string type. MySQL performs flexible and unexpected
+    matching when a query is performed on these types and the provided
+    value is an integer, which can cause queries to include unexpected
+    objects in their results. This problem cannot occur if you always
+    return a string type from :meth:`.get_prep_value`.
+
 Converting query values to database values
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
Index: python-django/docs/ref/databases.txt
===================================================================
--- python-django.orig/docs/ref/databases.txt
+++ python-django/docs/ref/databases.txt
@@ -343,6 +343,22 @@ Furthermore, if you are using a version
 column types have a maximum length restriction of 255 characters, regardless
 of whether ``unique=True`` is specified or not.
 
+Automatic typecasting can cause unexpected results
+--------------------------------------------------
+
+When performing a query on a string type, but with an integer value, MySQL will
+coerce the types of all values in the table to an integer before performing the
+comparison. If your table contains the values ``'abc'``, ``'def'`` and you
+query for ``WHERE mycolumn=0``, both rows will match. Similarly, ``WHERE mycolumn=1``
+will match the value ``'abc1'``. Therefore, string type fields included in Django
+will always cast the value to a string before using it in a query.
+
+If you implement custom model fields that inherit from :class:`~django.db.models.Field`
+directly, are overriding :meth:`~django.db.models.Field.get_prep_value`, or use
+:meth:`extra() <django.db.models.query.QuerySet.extra>` or
+:meth:`raw() <django.db.models.Manager.raw>`, you should ensure that you
+perform the appropriate typecasting.
+
 .. _sqlite-notes:
 
 SQLite notes
Index: python-django/docs/ref/models/querysets.txt
===================================================================
--- python-django.orig/docs/ref/models/querysets.txt
+++ python-django/docs/ref/models/querysets.txt
@@ -835,6 +835,16 @@ of the arguments is required, but you sh
 
         Entry.objects.extra(where=['headline=%s'], params=['Lennon'])
 
+.. warning::
+
+    If you are performing queries on MySQL, note that MySQL's silent type coercion
+    may cause unexpected results when mixing types. If you query on a string
+    type column, but with an integer value, MySQL will coerce the types of all values
+    in the table to an integer before performing the comparison. For example, if your
+    table contains the values ``'abc'``, ``'def'`` and you query for ``WHERE mycolumn=0``,
+    both rows will match. To prevent this, perform the correct typecasting
+    before using the value in a query.
+
 ``defer(*fields)``
 ~~~~~~~~~~~~~~~~~~
 
Index: python-django/docs/topics/db/sql.txt
===================================================================
--- python-django.orig/docs/topics/db/sql.txt
+++ python-django/docs/topics/db/sql.txt
@@ -56,6 +56,16 @@ You could then execute custom SQL like s
     :attr:`~Options.db_table` option, which also lets you manually set the
     database table name.
 
+.. warning::
+
+    If you are performing queries on MySQL, note that MySQL's silent type coercion
+    may cause unexpected results when mixing types. If you query on a string
+    type column, but with an integer value, MySQL will coerce the types of all values
+    in the table to an integer before performing the comparison. For example, if your
+    table contains the values ``'abc'``, ``'def'`` and you query for ``WHERE mycolumn=0``,
+    both rows will match. To prevent this, perform the correct typecasting
+    before using the value in a query.
+
 Of course, this example isn't very exciting -- it's exactly the same as
 running ``Person.objects.all()``. However, ``raw()`` has a bunch of other
 options that make it very powerful.
