Fixed #12030 -- Validate integer field range at the model level.

Thanks to @timgraham for the review.
This commit is contained in:
Simon Charette 2014-03-03 20:12:42 -05:00
parent 9b7ba8af1b
commit 1506c71a95
8 changed files with 153 additions and 26 deletions

View File

@ -617,6 +617,16 @@ class BaseDatabaseOperations(object):
""" """
compiler_module = "django.db.models.sql.compiler" 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): def __init__(self, connection):
self.connection = connection self.connection = connection
self._cache = None self._cache = None
@ -1101,6 +1111,14 @@ class BaseDatabaseOperations(object):
""" """
return params 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) # Structure returned by the DB-API cursor.description interface (PEP 249)
FieldInfo = namedtuple('FieldInfo', FieldInfo = namedtuple('FieldInfo',

View File

@ -223,6 +223,12 @@ class DatabaseFeatures(BaseDatabaseFeatures):
class DatabaseOperations(BaseDatabaseOperations): class DatabaseOperations(BaseDatabaseOperations):
compiler_module = "django.db.backends.mysql.compiler" 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): def date_extract_sql(self, lookup_type, field_name):
# http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html # http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html
if lookup_type == 'week_day': if lookup_type == 'week_day':

View File

@ -121,6 +121,15 @@ class DatabaseFeatures(BaseDatabaseFeatures):
class DatabaseOperations(BaseDatabaseOperations): class DatabaseOperations(BaseDatabaseOperations):
compiler_module = "django.db.backends.oracle.compiler" 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): def autoinc_sql(self, table, column):
# To simulate auto-incrementing primary keys in Oracle, we have to # To simulate auto-incrementing primary keys in Oracle, we have to
# create a sequence and a trigger. # create a sequence and a trigger.

View File

@ -292,6 +292,10 @@ class DatabaseOperations(BaseDatabaseOperations):
return 'django_power(%s)' % ','.join(sub_expressions) return 'django_power(%s)' % ','.join(sub_expressions)
return super(DatabaseOperations, self).combine_expression(connector, 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): class DatabaseWrapper(BaseDatabaseWrapper):
vendor = 'sqlite' vendor = 'sqlite'

View File

@ -1561,6 +1561,16 @@ class IntegerField(Field):
} }
description = _("Integer") 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): def get_prep_value(self, value):
value = super(IntegerField, self).get_prep_value(value) value = super(IntegerField, self).get_prep_value(value)
if value is None: if value is None:

View File

@ -678,6 +678,11 @@ Models
Previously this used to work if the field accepted integers as input as it Previously this used to work if the field accepted integers as input as it
took the primary key. 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 Signals
^^^^^^^ ^^^^^^^

View File

@ -55,11 +55,27 @@ class BigS(models.Model):
s = models.SlugField(max_length=255) 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() value = models.BigIntegerField()
null_value = models.BigIntegerField(null=True, blank=True) 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): class Post(models.Model):
title = models.CharField(max_length=100) title = models.CharField(max_length=100)
body = models.TextField() body = models.TextField()

View File

@ -7,6 +7,7 @@ import warnings
from django import test from django import test
from django import forms from django import forms
from django.core import validators
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import connection, transaction, models, IntegrityError from django.db import connection, transaction, models, IntegrityError
from django.db.models.fields import ( from django.db.models.fields import (
@ -21,9 +22,10 @@ from django.utils import six
from django.utils.functional import lazy from django.utils.functional import lazy
from .models import ( from .models import (
Foo, Bar, Whiz, BigD, BigS, BigInt, Post, NullBooleanModel, Foo, Bar, Whiz, BigD, BigS, BigIntegerModel, Post, NullBooleanModel,
BooleanModel, PrimaryKeyCharModel, DataModel, Document, RenamedField, BooleanModel, PrimaryKeyCharModel, DataModel, Document, RenamedField,
DateTimeModel, VerboseNameField, FksToBooleans, FkToChar, FloatModel) DateTimeModel, VerboseNameField, FksToBooleans, FkToChar, FloatModel,
SmallIntegerModel, IntegerModel, PositiveSmallIntegerModel, PositiveIntegerModel)
class BasicFieldTests(test.TestCase): class BasicFieldTests(test.TestCase):
@ -131,7 +133,6 @@ class DecimalFieldTests(test.TestCase):
self.assertEqual(f._format(None), None) self.assertEqual(f._format(None), None)
def test_get_db_prep_lookup(self): def test_get_db_prep_lookup(self):
from django.db import connection
f = models.DecimalField(max_digits=5, decimal_places=1) f = models.DecimalField(max_digits=5, decimal_places=1)
self.assertEqual(f.get_db_prep_lookup('exact', None, connection=connection), [None]) self.assertEqual(f.get_db_prep_lookup('exact', None, connection=connection), [None])
@ -212,7 +213,6 @@ class DateTimeFieldTests(unittest.TestCase):
class BooleanFieldTests(unittest.TestCase): class BooleanFieldTests(unittest.TestCase):
def _test_get_db_prep_lookup(self, f): 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', 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])
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) self.assertRaises(ValidationError, f.clean, None, None)
class BigIntegerFieldTests(test.TestCase): class IntegerFieldTests(test.TestCase):
def test_limits(self): model = IntegerModel
# Ensure that values that are right at the limits can be saved documented_range = (-2147483648, 2147483647)
# and then retrieved without corruption.
maxval = 9223372036854775807 def test_documented_range(self):
minval = -maxval - 1 """
BigInt.objects.create(value=maxval) Ensure that values within the documented safe range pass validation,
qs = BigInt.objects.filter(value__gte=maxval) 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.count(), 1)
self.assertEqual(qs[0].value, maxval) self.assertEqual(qs[0].value, min_value)
BigInt.objects.create(value=minval)
qs = BigInt.objects.filter(value__lte=minval) 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.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): def test_types(self):
b = BigInt(value=0) instance = self.model(value=0)
self.assertIsInstance(b.value, six.integer_types) self.assertIsInstance(instance.value, six.integer_types)
b.save() instance.save()
self.assertIsInstance(b.value, six.integer_types) self.assertIsInstance(instance.value, six.integer_types)
b = BigInt.objects.all()[0] instance = self.model.objects.get()
self.assertIsInstance(b.value, six.integer_types) self.assertIsInstance(instance.value, six.integer_types)
def test_coercing(self): def test_coercing(self):
BigInt.objects.create(value='10') self.model.objects.create(value='10')
b = BigInt.objects.get(value='10') instance = self.model.objects.get(value='10')
self.assertEqual(b.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): class TypeCoercionTests(test.TestCase):