mirror of
				https://github.com/django/django.git
				synced 2025-11-03 21:25:09 +00:00 
			
		
		
		
	Fixed #12030 -- Validate integer field range at the model level.
Thanks to @timgraham for the review.
This commit is contained in:
		
							parent
							
								
									9b7ba8af1b
								
							
						
					
					
						commit
						1506c71a95
					
				
					 8 changed files with 153 additions and 26 deletions
				
			
		| 
						 | 
				
			
			@ -617,6 +617,16 @@ class BaseDatabaseOperations(object):
 | 
			
		|||
    """
 | 
			
		||||
    compiler_module = "django.db.models.sql.compiler"
 | 
			
		||||
 | 
			
		||||
    # Integer field safe ranges by `internal_type` as documented
 | 
			
		||||
    # in docs/ref/models/fields.txt.
 | 
			
		||||
    integer_field_ranges = {
 | 
			
		||||
        'SmallIntegerField': (-32768, 32767),
 | 
			
		||||
        'IntegerField': (-2147483648, 2147483647),
 | 
			
		||||
        'BigIntegerField': (-9223372036854775808, 9223372036854775807),
 | 
			
		||||
        'PositiveSmallIntegerField': (0, 32767),
 | 
			
		||||
        'PositiveIntegerField': (0, 2147483647),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def __init__(self, connection):
 | 
			
		||||
        self.connection = connection
 | 
			
		||||
        self._cache = None
 | 
			
		||||
| 
						 | 
				
			
			@ -1101,6 +1111,14 @@ class BaseDatabaseOperations(object):
 | 
			
		|||
        """
 | 
			
		||||
        return params
 | 
			
		||||
 | 
			
		||||
    def integer_field_range(self, internal_type):
 | 
			
		||||
        """
 | 
			
		||||
        Given an integer field internal type (e.g. 'PositiveIntegerField'),
 | 
			
		||||
        returns a tuple of the (min_value, max_value) form representing the
 | 
			
		||||
        range of the column type bound to the field.
 | 
			
		||||
        """
 | 
			
		||||
        return self.integer_field_ranges[internal_type]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Structure returned by the DB-API cursor.description interface (PEP 249)
 | 
			
		||||
FieldInfo = namedtuple('FieldInfo',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -223,6 +223,12 @@ class DatabaseFeatures(BaseDatabaseFeatures):
 | 
			
		|||
class DatabaseOperations(BaseDatabaseOperations):
 | 
			
		||||
    compiler_module = "django.db.backends.mysql.compiler"
 | 
			
		||||
 | 
			
		||||
    # MySQL stores positive fields as UNSIGNED ints.
 | 
			
		||||
    integer_field_ranges = dict(BaseDatabaseOperations.integer_field_ranges,
 | 
			
		||||
        PositiveSmallIntegerField=(0, 4294967295),
 | 
			
		||||
        PositiveIntegerField=(0, 18446744073709551615),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def date_extract_sql(self, lookup_type, field_name):
 | 
			
		||||
        # http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html
 | 
			
		||||
        if lookup_type == 'week_day':
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -121,6 +121,15 @@ class DatabaseFeatures(BaseDatabaseFeatures):
 | 
			
		|||
class DatabaseOperations(BaseDatabaseOperations):
 | 
			
		||||
    compiler_module = "django.db.backends.oracle.compiler"
 | 
			
		||||
 | 
			
		||||
    # Oracle uses NUMBER(11) and NUMBER(19) for integer fields.
 | 
			
		||||
    integer_field_ranges = {
 | 
			
		||||
        'SmallIntegerField': (-99999999999, 99999999999),
 | 
			
		||||
        'IntegerField': (-99999999999, 99999999999),
 | 
			
		||||
        'BigIntegerField': (-9999999999999999999, 9999999999999999999),
 | 
			
		||||
        'PositiveSmallIntegerField': (0, 99999999999),
 | 
			
		||||
        'PositiveIntegerField': (0, 99999999999),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def autoinc_sql(self, table, column):
 | 
			
		||||
        # To simulate auto-incrementing primary keys in Oracle, we have to
 | 
			
		||||
        # create a sequence and a trigger.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -292,6 +292,10 @@ class DatabaseOperations(BaseDatabaseOperations):
 | 
			
		|||
            return 'django_power(%s)' % ','.join(sub_expressions)
 | 
			
		||||
        return super(DatabaseOperations, self).combine_expression(connector, sub_expressions)
 | 
			
		||||
 | 
			
		||||
    def integer_field_range(self, internal_type):
 | 
			
		||||
        # SQLite doesn't enforce any integer constraints
 | 
			
		||||
        return (None, None)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DatabaseWrapper(BaseDatabaseWrapper):
 | 
			
		||||
    vendor = 'sqlite'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1561,6 +1561,16 @@ class IntegerField(Field):
 | 
			
		|||
    }
 | 
			
		||||
    description = _("Integer")
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
        field_validators = kwargs.setdefault('validators', [])
 | 
			
		||||
        internal_type = self.get_internal_type()
 | 
			
		||||
        min_value, max_value = connection.ops.integer_field_range(internal_type)
 | 
			
		||||
        if min_value is not None:
 | 
			
		||||
            field_validators.append(validators.MinValueValidator(min_value))
 | 
			
		||||
        if max_value is not None:
 | 
			
		||||
            field_validators.append(validators.MaxValueValidator(max_value))
 | 
			
		||||
        super(IntegerField, self).__init__(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
    def get_prep_value(self, value):
 | 
			
		||||
        value = super(IntegerField, self).get_prep_value(value)
 | 
			
		||||
        if value is None:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -678,6 +678,11 @@ Models
 | 
			
		|||
  Previously this used to work if the field accepted integers as input as it
 | 
			
		||||
  took the primary key.
 | 
			
		||||
 | 
			
		||||
* Integer fields are now validated against database backend specific min and
 | 
			
		||||
  max values based on their :meth:`internal_type <django.db.models.Field.get_internal_type>`.
 | 
			
		||||
  Previously model field validation didn't prevent values out of their associated
 | 
			
		||||
  column data type range from being saved resulting in an integrity error.
 | 
			
		||||
 | 
			
		||||
Signals
 | 
			
		||||
^^^^^^^
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -55,11 +55,27 @@ class BigS(models.Model):
 | 
			
		|||
    s = models.SlugField(max_length=255)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BigInt(models.Model):
 | 
			
		||||
class SmallIntegerModel(models.Model):
 | 
			
		||||
    value = models.SmallIntegerField()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class IntegerModel(models.Model):
 | 
			
		||||
    value = models.IntegerField()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BigIntegerModel(models.Model):
 | 
			
		||||
    value = models.BigIntegerField()
 | 
			
		||||
    null_value = models.BigIntegerField(null=True, blank=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PositiveSmallIntegerModel(models.Model):
 | 
			
		||||
    value = models.PositiveSmallIntegerField()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PositiveIntegerModel(models.Model):
 | 
			
		||||
    value = models.PositiveIntegerField()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Post(models.Model):
 | 
			
		||||
    title = models.CharField(max_length=100)
 | 
			
		||||
    body = models.TextField()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,7 @@ import warnings
 | 
			
		|||
 | 
			
		||||
from django import test
 | 
			
		||||
from django import forms
 | 
			
		||||
from django.core import validators
 | 
			
		||||
from django.core.exceptions import ValidationError
 | 
			
		||||
from django.db import connection, transaction, models, IntegrityError
 | 
			
		||||
from django.db.models.fields import (
 | 
			
		||||
| 
						 | 
				
			
			@ -21,9 +22,10 @@ from django.utils import six
 | 
			
		|||
from django.utils.functional import lazy
 | 
			
		||||
 | 
			
		||||
from .models import (
 | 
			
		||||
    Foo, Bar, Whiz, BigD, BigS, BigInt, Post, NullBooleanModel,
 | 
			
		||||
    Foo, Bar, Whiz, BigD, BigS, BigIntegerModel, Post, NullBooleanModel,
 | 
			
		||||
    BooleanModel, PrimaryKeyCharModel, DataModel, Document, RenamedField,
 | 
			
		||||
    DateTimeModel, VerboseNameField, FksToBooleans, FkToChar, FloatModel)
 | 
			
		||||
    DateTimeModel, VerboseNameField, FksToBooleans, FkToChar, FloatModel,
 | 
			
		||||
    SmallIntegerModel, IntegerModel, PositiveSmallIntegerModel, PositiveIntegerModel)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BasicFieldTests(test.TestCase):
 | 
			
		||||
| 
						 | 
				
			
			@ -131,7 +133,6 @@ class DecimalFieldTests(test.TestCase):
 | 
			
		|||
        self.assertEqual(f._format(None), None)
 | 
			
		||||
 | 
			
		||||
    def test_get_db_prep_lookup(self):
 | 
			
		||||
        from django.db import connection
 | 
			
		||||
        f = models.DecimalField(max_digits=5, decimal_places=1)
 | 
			
		||||
        self.assertEqual(f.get_db_prep_lookup('exact', None, connection=connection), [None])
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -212,7 +213,6 @@ class DateTimeFieldTests(unittest.TestCase):
 | 
			
		|||
 | 
			
		||||
class BooleanFieldTests(unittest.TestCase):
 | 
			
		||||
    def _test_get_db_prep_lookup(self, f):
 | 
			
		||||
        from django.db import connection
 | 
			
		||||
        self.assertEqual(f.get_db_prep_lookup('exact', True, connection=connection), [True])
 | 
			
		||||
        self.assertEqual(f.get_db_prep_lookup('exact', '1', connection=connection), [True])
 | 
			
		||||
        self.assertEqual(f.get_db_prep_lookup('exact', 1, connection=connection), [True])
 | 
			
		||||
| 
						 | 
				
			
			@ -451,33 +451,92 @@ class ValidationTest(test.TestCase):
 | 
			
		|||
        self.assertRaises(ValidationError, f.clean, None, None)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BigIntegerFieldTests(test.TestCase):
 | 
			
		||||
    def test_limits(self):
 | 
			
		||||
        # Ensure that values that are right at the limits can be saved
 | 
			
		||||
        # and then retrieved without corruption.
 | 
			
		||||
        maxval = 9223372036854775807
 | 
			
		||||
        minval = -maxval - 1
 | 
			
		||||
        BigInt.objects.create(value=maxval)
 | 
			
		||||
        qs = BigInt.objects.filter(value__gte=maxval)
 | 
			
		||||
class IntegerFieldTests(test.TestCase):
 | 
			
		||||
    model = IntegerModel
 | 
			
		||||
    documented_range = (-2147483648, 2147483647)
 | 
			
		||||
 | 
			
		||||
    def test_documented_range(self):
 | 
			
		||||
        """
 | 
			
		||||
        Ensure that values within the documented safe range pass validation,
 | 
			
		||||
        can be saved and retrieved without corruption.
 | 
			
		||||
        """
 | 
			
		||||
        min_value, max_value = self.documented_range
 | 
			
		||||
 | 
			
		||||
        instance = self.model(value=min_value)
 | 
			
		||||
        instance.full_clean()
 | 
			
		||||
        instance.save()
 | 
			
		||||
        qs = self.model.objects.filter(value__lte=min_value)
 | 
			
		||||
        self.assertEqual(qs.count(), 1)
 | 
			
		||||
        self.assertEqual(qs[0].value, maxval)
 | 
			
		||||
        BigInt.objects.create(value=minval)
 | 
			
		||||
        qs = BigInt.objects.filter(value__lte=minval)
 | 
			
		||||
        self.assertEqual(qs[0].value, min_value)
 | 
			
		||||
 | 
			
		||||
        instance = self.model(value=max_value)
 | 
			
		||||
        instance.full_clean()
 | 
			
		||||
        instance.save()
 | 
			
		||||
        qs = self.model.objects.filter(value__gte=max_value)
 | 
			
		||||
        self.assertEqual(qs.count(), 1)
 | 
			
		||||
        self.assertEqual(qs[0].value, minval)
 | 
			
		||||
        self.assertEqual(qs[0].value, max_value)
 | 
			
		||||
 | 
			
		||||
    def test_backend_range_validation(self):
 | 
			
		||||
        """
 | 
			
		||||
        Ensure that backend specific range are enforced at the model
 | 
			
		||||
        validation level. ref #12030.
 | 
			
		||||
        """
 | 
			
		||||
        field = self.model._meta.get_field('value')
 | 
			
		||||
        internal_type = field.get_internal_type()
 | 
			
		||||
        min_value, max_value = connection.ops.integer_field_range(internal_type)
 | 
			
		||||
 | 
			
		||||
        if min_value is not None:
 | 
			
		||||
            instance = self.model(value=min_value - 1)
 | 
			
		||||
            expected_message = validators.MinValueValidator.message % {
 | 
			
		||||
                'limit_value': min_value
 | 
			
		||||
            }
 | 
			
		||||
            with self.assertRaisesMessage(ValidationError, expected_message):
 | 
			
		||||
                instance.full_clean()
 | 
			
		||||
            instance.value = min_value
 | 
			
		||||
            instance.full_clean()
 | 
			
		||||
 | 
			
		||||
        if max_value is not None:
 | 
			
		||||
            instance = self.model(value=max_value + 1)
 | 
			
		||||
            expected_message = validators.MaxValueValidator.message % {
 | 
			
		||||
                'limit_value': max_value
 | 
			
		||||
            }
 | 
			
		||||
            with self.assertRaisesMessage(ValidationError, expected_message):
 | 
			
		||||
                instance.full_clean()
 | 
			
		||||
            instance.value = max_value
 | 
			
		||||
            instance.full_clean()
 | 
			
		||||
 | 
			
		||||
    def test_types(self):
 | 
			
		||||
        b = BigInt(value=0)
 | 
			
		||||
        self.assertIsInstance(b.value, six.integer_types)
 | 
			
		||||
        b.save()
 | 
			
		||||
        self.assertIsInstance(b.value, six.integer_types)
 | 
			
		||||
        b = BigInt.objects.all()[0]
 | 
			
		||||
        self.assertIsInstance(b.value, six.integer_types)
 | 
			
		||||
        instance = self.model(value=0)
 | 
			
		||||
        self.assertIsInstance(instance.value, six.integer_types)
 | 
			
		||||
        instance.save()
 | 
			
		||||
        self.assertIsInstance(instance.value, six.integer_types)
 | 
			
		||||
        instance = self.model.objects.get()
 | 
			
		||||
        self.assertIsInstance(instance.value, six.integer_types)
 | 
			
		||||
 | 
			
		||||
    def test_coercing(self):
 | 
			
		||||
        BigInt.objects.create(value='10')
 | 
			
		||||
        b = BigInt.objects.get(value='10')
 | 
			
		||||
        self.assertEqual(b.value, 10)
 | 
			
		||||
        self.model.objects.create(value='10')
 | 
			
		||||
        instance = self.model.objects.get(value='10')
 | 
			
		||||
        self.assertEqual(instance.value, 10)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SmallIntegerFieldTests(IntegerFieldTests):
 | 
			
		||||
    model = SmallIntegerModel
 | 
			
		||||
    documented_range = (-32768, 32767)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BigIntegerFieldTests(IntegerFieldTests):
 | 
			
		||||
    model = BigIntegerModel
 | 
			
		||||
    documented_range = (-9223372036854775808, 9223372036854775807)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PositiveSmallIntegerFieldTests(IntegerFieldTests):
 | 
			
		||||
    model = PositiveSmallIntegerModel
 | 
			
		||||
    documented_range = (0, 32767)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PositiveIntegerFieldTests(IntegerFieldTests):
 | 
			
		||||
    model = PositiveIntegerModel
 | 
			
		||||
    documented_range = (0, 2147483647)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TypeCoercionTests(test.TestCase):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue