mirror of
https://github.com/django/django.git
synced 2025-11-17 02:24:22 +00:00
Fixed #24920 -- Added support for DecimalField with no precision.
Thanks Lily for the review.
This commit is contained in:
parent
5c60763561
commit
f21882a68a
16 changed files with 205 additions and 78 deletions
|
|
@ -359,21 +359,15 @@ class Command(BaseCommand):
|
|||
if field_type in {"CharField", "TextField"} and row.collation:
|
||||
field_params["db_collation"] = row.collation
|
||||
|
||||
if field_type == "DecimalField":
|
||||
if row.precision is None or row.scale is None:
|
||||
field_notes.append(
|
||||
"max_digits and decimal_places have been guessed, as this "
|
||||
"database handles decimal fields as float"
|
||||
)
|
||||
field_params["max_digits"] = (
|
||||
row.precision if row.precision is not None else 10
|
||||
)
|
||||
field_params["decimal_places"] = (
|
||||
row.scale if row.scale is not None else 5
|
||||
)
|
||||
else:
|
||||
field_params["max_digits"] = row.precision
|
||||
field_params["decimal_places"] = row.scale
|
||||
if field_type == "DecimalField" and (
|
||||
# This can generate DecimalField with only one of max_digits and
|
||||
# decimal_fields specified. This configuration would be incorrect,
|
||||
# but nothing more correct that could be generated.
|
||||
row.precision is not None
|
||||
or row.scale is not None
|
||||
):
|
||||
field_params["max_digits"] = row.precision
|
||||
field_params["decimal_places"] = row.scale
|
||||
|
||||
return field_type, field_params, field_notes
|
||||
|
||||
|
|
|
|||
|
|
@ -383,6 +383,9 @@ class BaseDatabaseFeatures:
|
|||
# Does the backend support unlimited character columns?
|
||||
supports_unlimited_charfield = False
|
||||
|
||||
# Does the backend support numeric columns with no precision?
|
||||
supports_no_precision_decimalfield = False
|
||||
|
||||
# Does the backend support native tuple lookups (=, >, <, IN)?
|
||||
supports_tuple_lookups = True
|
||||
|
||||
|
|
|
|||
|
|
@ -106,6 +106,12 @@ class _UninitializedOperatorsDescriptor:
|
|||
return instance.__dict__["operators"]
|
||||
|
||||
|
||||
def _get_decimal_column(data):
|
||||
if data["max_digits"] is None and data["decimal_places"] is None:
|
||||
return "NUMBER"
|
||||
return "NUMBER(%(max_digits)s, %(decimal_places)s)" % data
|
||||
|
||||
|
||||
class DatabaseWrapper(BaseDatabaseWrapper):
|
||||
vendor = "oracle"
|
||||
display_name = "Oracle"
|
||||
|
|
@ -125,7 +131,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
|||
"CharField": "NVARCHAR2(%(max_length)s)",
|
||||
"DateField": "DATE",
|
||||
"DateTimeField": "TIMESTAMP",
|
||||
"DecimalField": "NUMBER(%(max_digits)s, %(decimal_places)s)",
|
||||
"DecimalField": _get_decimal_column,
|
||||
"DurationField": "INTERVAL DAY(9) TO SECOND(6)",
|
||||
"FileField": "NVARCHAR2(%(max_length)s)",
|
||||
"FilePathField": "NVARCHAR2(%(max_length)s)",
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
supports_json_negative_indexing = False
|
||||
supports_collation_on_textfield = False
|
||||
supports_on_delete_db_default = False
|
||||
supports_no_precision_decimalfield = True
|
||||
test_now_utc_template = "CURRENT_TIMESTAMP AT TIME ZONE 'UTC'"
|
||||
django_test_expected_failures = {
|
||||
# A bug in Django/oracledb with respect to string handling (#23843).
|
||||
|
|
|
|||
|
|
@ -194,14 +194,21 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
|||
comment,
|
||||
) = field_map[name]
|
||||
name %= {} # oracledb, for some reason, doubles percent signs.
|
||||
if desc[1] == oracledb.NUMBER and desc[5] == -127 and desc[4] == 0:
|
||||
# DecimalField with no precision.
|
||||
precision = None
|
||||
scale = None
|
||||
else:
|
||||
precision = desc[4] or 0
|
||||
scale = desc[5] or 0
|
||||
description.append(
|
||||
FieldInfo(
|
||||
self.identifier_converter(name),
|
||||
desc[1],
|
||||
display_size,
|
||||
desc[3],
|
||||
desc[4] or 0,
|
||||
desc[5] or 0,
|
||||
precision,
|
||||
scale,
|
||||
*desc[6:],
|
||||
default,
|
||||
collation,
|
||||
|
|
|
|||
|
|
@ -89,6 +89,12 @@ def _get_varchar_column(data):
|
|||
return "varchar(%(max_length)s)" % data
|
||||
|
||||
|
||||
def _get_decimal_column(data):
|
||||
if data["max_digits"] is None and data["decimal_places"] is None:
|
||||
return "numeric"
|
||||
return "numeric(%(max_digits)s, %(decimal_places)s)" % data
|
||||
|
||||
|
||||
class DatabaseWrapper(BaseDatabaseWrapper):
|
||||
vendor = "postgresql"
|
||||
display_name = "PostgreSQL"
|
||||
|
|
@ -105,7 +111,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
|||
"CharField": _get_varchar_column,
|
||||
"DateField": "date",
|
||||
"DateTimeField": "timestamp with time zone",
|
||||
"DecimalField": "numeric(%(max_digits)s, %(decimal_places)s)",
|
||||
"DecimalField": _get_decimal_column,
|
||||
"DurationField": "interval",
|
||||
"FileField": "varchar(%(max_length)s)",
|
||||
"FilePathField": "varchar(%(max_length)s)",
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
supports_covering_indexes = True
|
||||
supports_stored_generated_columns = True
|
||||
supports_nulls_distinct_unique_constraints = True
|
||||
supports_no_precision_decimalfield = True
|
||||
can_rename_index = True
|
||||
test_collations = {
|
||||
"deterministic": "C",
|
||||
|
|
|
|||
|
|
@ -137,8 +137,10 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
|||
# display_size is always None on psycopg2.
|
||||
line.internal_size if line.display_size is None else line.display_size,
|
||||
line.internal_size,
|
||||
line.precision,
|
||||
line.scale,
|
||||
# precision and scale are always 2^16 - 1 on psycopg2 for
|
||||
# DecimalFields with no precision.
|
||||
None if line.precision == 2**16 - 1 else line.precision,
|
||||
None if line.scale == 2**16 - 1 else line.scale,
|
||||
*field_map[line.name],
|
||||
)
|
||||
for line in cursor.description
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
insert_test_table_with_defaults = 'INSERT INTO {} ("null") VALUES (1)'
|
||||
supports_default_keyword_in_insert = False
|
||||
supports_unlimited_charfield = True
|
||||
supports_no_precision_decimalfield = True
|
||||
can_return_columns_from_insert = True
|
||||
can_return_rows_from_bulk_insert = True
|
||||
can_return_rows_from_update = True
|
||||
|
|
|
|||
|
|
@ -1725,54 +1725,84 @@ class DecimalField(Field):
|
|||
return errors
|
||||
|
||||
def _check_decimal_places(self):
|
||||
try:
|
||||
decimal_places = int(self.decimal_places)
|
||||
if decimal_places < 0:
|
||||
raise ValueError()
|
||||
except TypeError:
|
||||
return [
|
||||
checks.Error(
|
||||
"DecimalFields must define a 'decimal_places' attribute.",
|
||||
obj=self,
|
||||
id="fields.E130",
|
||||
)
|
||||
]
|
||||
except ValueError:
|
||||
return [
|
||||
checks.Error(
|
||||
"'decimal_places' must be a non-negative integer.",
|
||||
obj=self,
|
||||
id="fields.E131",
|
||||
)
|
||||
]
|
||||
if self.decimal_places is None:
|
||||
if not (
|
||||
connection.features.supports_no_precision_decimalfield
|
||||
or "supports_no_precision_decimalfield"
|
||||
in self.model._meta.required_db_features
|
||||
):
|
||||
return [
|
||||
checks.Error(
|
||||
"DecimalFields must define a 'decimal_places' attribute.",
|
||||
obj=self,
|
||||
id="fields.E130",
|
||||
)
|
||||
]
|
||||
elif self.max_digits is not None:
|
||||
return [
|
||||
checks.Error(
|
||||
"DecimalField’s max_digits and decimal_places must both "
|
||||
"be defined or both omitted.",
|
||||
obj=self,
|
||||
id="fields.E135",
|
||||
),
|
||||
]
|
||||
else:
|
||||
return []
|
||||
try:
|
||||
decimal_places = int(self.decimal_places)
|
||||
if decimal_places < 0:
|
||||
raise ValueError()
|
||||
except ValueError:
|
||||
return [
|
||||
checks.Error(
|
||||
"'decimal_places' must be a non-negative integer.",
|
||||
obj=self,
|
||||
id="fields.E131",
|
||||
)
|
||||
]
|
||||
return []
|
||||
|
||||
def _check_max_digits(self):
|
||||
try:
|
||||
max_digits = int(self.max_digits)
|
||||
if max_digits <= 0:
|
||||
raise ValueError()
|
||||
except TypeError:
|
||||
return [
|
||||
checks.Error(
|
||||
"DecimalFields must define a 'max_digits' attribute.",
|
||||
obj=self,
|
||||
id="fields.E132",
|
||||
)
|
||||
]
|
||||
except ValueError:
|
||||
return [
|
||||
checks.Error(
|
||||
"'max_digits' must be a positive integer.",
|
||||
obj=self,
|
||||
id="fields.E133",
|
||||
)
|
||||
]
|
||||
if self.max_digits is None:
|
||||
if not (
|
||||
connection.features.supports_no_precision_decimalfield
|
||||
or "supports_no_precision_decimalfield"
|
||||
in self.model._meta.required_db_features
|
||||
):
|
||||
return [
|
||||
checks.Error(
|
||||
"DecimalFields must define a 'max_digits' attribute.",
|
||||
obj=self,
|
||||
id="fields.E132",
|
||||
)
|
||||
]
|
||||
elif self.decimal_places is not None:
|
||||
return [
|
||||
checks.Error(
|
||||
"DecimalField’s max_digits and decimal_places must both "
|
||||
"be defined or both omitted.",
|
||||
obj=self,
|
||||
id="fields.E135",
|
||||
),
|
||||
]
|
||||
else:
|
||||
return []
|
||||
try:
|
||||
max_digits = int(self.max_digits)
|
||||
if max_digits <= 0:
|
||||
raise ValueError()
|
||||
except ValueError:
|
||||
return [
|
||||
checks.Error(
|
||||
"'max_digits' must be a positive integer.",
|
||||
obj=self,
|
||||
id="fields.E133",
|
||||
)
|
||||
]
|
||||
return []
|
||||
|
||||
def _check_decimal_places_and_max_digits(self, **kwargs):
|
||||
if self.decimal_places is None or self.max_digits is None:
|
||||
return []
|
||||
if int(self.decimal_places) > int(self.max_digits):
|
||||
return [
|
||||
checks.Error(
|
||||
|
|
|
|||
|
|
@ -196,6 +196,8 @@ Model fields
|
|||
* **fields.E133**: ``max_digits`` must be a positive integer.
|
||||
* **fields.E134**: ``max_digits`` must be greater or equal to
|
||||
``decimal_places``.
|
||||
* **fields.E135**: ``DecimalField``’s ``max_digits`` and ``decimal_places``
|
||||
must both be defined or both omitted.
|
||||
* **fields.E140**: ``FilePathField``\s must have either ``allow_files`` or
|
||||
``allow_folders`` set to True.
|
||||
* **fields.E150**: ``GenericIPAddressField``\s cannot have ``blank=True`` if
|
||||
|
|
|
|||
|
|
@ -862,16 +862,22 @@ A fixed-precision decimal number, represented in Python by a
|
|||
:class:`~decimal.Decimal` instance. It validates the input using
|
||||
:class:`~django.core.validators.DecimalValidator`.
|
||||
|
||||
Has the following **required** arguments:
|
||||
Has the following arguments:
|
||||
|
||||
.. attribute:: DecimalField.max_digits
|
||||
|
||||
The maximum number of digits allowed in the number. Note that this number
|
||||
must be greater than or equal to ``decimal_places``.
|
||||
must be greater than or equal to ``decimal_places``. It's always required
|
||||
on MySQL because this database doesn't support numeric fields with no
|
||||
precision. It's also required for all database backends when
|
||||
:attr:`~DecimalField.decimal_places` is provided.
|
||||
|
||||
.. attribute:: DecimalField.decimal_places
|
||||
|
||||
The number of decimal places to store with the number.
|
||||
The number of decimal places to store with the number. It's always required
|
||||
on MySQL because this database doesn't support numeric fields with no
|
||||
precision. It's also required for all database backends when
|
||||
:attr:`~DecimalField.max_digits` is provided.
|
||||
|
||||
For example, to store numbers up to ``999.99`` with a resolution of 2 decimal
|
||||
places, you'd use::
|
||||
|
|
@ -895,6 +901,11 @@ when :attr:`~django.forms.Field.localize` is ``False`` or
|
|||
should also be aware of :ref:`SQLite limitations <sqlite-decimal-handling>`
|
||||
of decimal fields.
|
||||
|
||||
.. versionchanged:: 6.1
|
||||
|
||||
Support for ``DecimalField`` with no precision was added on Oracle,
|
||||
PostgreSQL, and SQLite.
|
||||
|
||||
``DurationField``
|
||||
-----------------
|
||||
|
||||
|
|
|
|||
|
|
@ -247,6 +247,11 @@ Models
|
|||
top-level or nested JSON ``null`` values. See
|
||||
:ref:`storing-and-querying-for-none` for usage examples and some caveats.
|
||||
|
||||
* :attr:`DecimalField.max_digits <django.db.models.DecimalField.max_digits>`
|
||||
and :attr:`DecimalField.decimal_places
|
||||
<django.db.models.DecimalField.decimal_places>` are no longer required to be
|
||||
set on Oracle, PostgreSQL, and SQLite.
|
||||
|
||||
Pagination
|
||||
~~~~~~~~~~
|
||||
|
||||
|
|
|
|||
|
|
@ -121,6 +121,15 @@ class CharFieldUnlimited(models.Model):
|
|||
required_db_features = {"supports_unlimited_charfield"}
|
||||
|
||||
|
||||
class DecimalFieldNoPrec(models.Model):
|
||||
decimal_field_no_precision = models.DecimalField(
|
||||
max_digits=None, decimal_places=None
|
||||
)
|
||||
|
||||
class Meta:
|
||||
required_db_features = {"supports_no_precision_decimalfield"}
|
||||
|
||||
|
||||
class UniqueTogether(models.Model):
|
||||
field1 = models.IntegerField()
|
||||
field2 = models.CharField(max_length=10)
|
||||
|
|
|
|||
|
|
@ -202,6 +202,13 @@ class InspectDBTestCase(TestCase):
|
|||
output = out.getvalue()
|
||||
self.assertIn("char_field = models.CharField()", output)
|
||||
|
||||
@skipUnlessDBFeature("supports_no_precision_decimalfield")
|
||||
def test_decimal_field_no_precision(self):
|
||||
out = StringIO()
|
||||
call_command("inspectdb", "inspectdb_decimalfieldnoprec", stdout=out)
|
||||
output = out.getvalue()
|
||||
self.assertIn("decimal_field_no_precision = models.DecimalField()", output)
|
||||
|
||||
def test_number_field_types(self):
|
||||
"""Test introspection of various Django field types"""
|
||||
assertFieldType = self.make_field_type_asserter()
|
||||
|
|
@ -228,13 +235,8 @@ class InspectDBTestCase(TestCase):
|
|||
assertFieldType(
|
||||
"decimal_field", "models.DecimalField(max_digits=6, decimal_places=1)"
|
||||
)
|
||||
else: # Guessed arguments on SQLite, see #5014
|
||||
assertFieldType(
|
||||
"decimal_field",
|
||||
"models.DecimalField(max_digits=10, decimal_places=5) "
|
||||
"# max_digits and decimal_places have been guessed, "
|
||||
"as this database handles decimal fields as float",
|
||||
)
|
||||
else:
|
||||
assertFieldType("decimal_field", "models.DecimalField()")
|
||||
|
||||
assertFieldType("float_field", "models.FloatField()")
|
||||
assertFieldType(
|
||||
|
|
|
|||
|
|
@ -599,15 +599,16 @@ class DateTimeFieldTests(SimpleTestCase):
|
|||
|
||||
|
||||
@isolate_apps("invalid_models_tests")
|
||||
class DecimalFieldTests(SimpleTestCase):
|
||||
def test_required_attributes(self):
|
||||
class DecimalFieldTests(TestCase):
|
||||
def test_both_attributes_omitted(self):
|
||||
class Model(models.Model):
|
||||
field = models.DecimalField()
|
||||
|
||||
field = Model._meta.get_field("field")
|
||||
self.assertEqual(
|
||||
field.check(),
|
||||
[
|
||||
if connection.features.supports_no_precision_decimalfield:
|
||||
expected = []
|
||||
else:
|
||||
expected = [
|
||||
Error(
|
||||
"DecimalFields must define a 'decimal_places' attribute.",
|
||||
obj=field,
|
||||
|
|
@ -618,6 +619,52 @@ class DecimalFieldTests(SimpleTestCase):
|
|||
obj=field,
|
||||
id="fields.E132",
|
||||
),
|
||||
]
|
||||
self.assertEqual(field.check(), expected)
|
||||
|
||||
def test_both_attributes_omitted_required_db_features(self):
|
||||
class Model(models.Model):
|
||||
field = models.DecimalField()
|
||||
|
||||
class Meta:
|
||||
required_db_features = {"supports_no_precision_decimalfield"}
|
||||
|
||||
field = Model._meta.get_field("field")
|
||||
self.assertEqual(field.check(databases=self.databases), [])
|
||||
|
||||
@skipUnlessDBFeature("supports_no_precision_decimalfield")
|
||||
def test_only_max_digits_defined(self):
|
||||
class Model(models.Model):
|
||||
field = models.DecimalField(max_digits=13)
|
||||
|
||||
field = Model._meta.get_field("field")
|
||||
self.assertEqual(
|
||||
field.check(),
|
||||
[
|
||||
Error(
|
||||
"DecimalField’s max_digits and decimal_places must both "
|
||||
"be defined or both omitted.",
|
||||
obj=field,
|
||||
id="fields.E135",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@skipUnlessDBFeature("supports_no_precision_decimalfield")
|
||||
def test_only_decimal_places_defined(self):
|
||||
class Model(models.Model):
|
||||
field = models.DecimalField(decimal_places=5)
|
||||
|
||||
field = Model._meta.get_field("field")
|
||||
self.assertEqual(
|
||||
field.check(),
|
||||
[
|
||||
Error(
|
||||
"DecimalField’s max_digits and decimal_places must both "
|
||||
"be defined or both omitted.",
|
||||
obj=field,
|
||||
id="fields.E135",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue