summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMariusz Felisiak <felisiak.mariusz@gmail.com>2025-11-17 13:43:47 +0100
committerGitHub <noreply@github.com>2025-11-17 13:43:47 +0100
commit1ce6e78dd4beed702f15fa0be798dd17a15d4ba8 (patch)
tree0078da5312fb69897bb6327040d51c3450d2fa3b
parent5c60763561c67924eff1069e1516b60a59d068d5 (diff)
Fixed #24920 -- Added support for DecimalField with no precision.
Thanks Lily for the review.
-rw-r--r--django/core/management/commands/inspectdb.py24
-rw-r--r--django/db/backends/base/features.py3
-rw-r--r--django/db/backends/oracle/base.py8
-rw-r--r--django/db/backends/oracle/features.py1
-rw-r--r--django/db/backends/oracle/introspection.py11
-rw-r--r--django/db/backends/postgresql/base.py8
-rw-r--r--django/db/backends/postgresql/features.py1
-rw-r--r--django/db/backends/postgresql/introspection.py6
-rw-r--r--django/db/backends/sqlite3/features.py1
-rw-r--r--django/db/models/fields/__init__.py114
-rw-r--r--docs/ref/checks.txt2
-rw-r--r--docs/ref/models/fields.txt17
-rw-r--r--docs/releases/6.1.txt5
-rw-r--r--tests/inspectdb/models.py9
-rw-r--r--tests/inspectdb/tests.py16
-rw-r--r--tests/invalid_models_tests/test_ordinary_fields.py57
16 files changed, 205 insertions, 78 deletions
diff --git a/django/core/management/commands/inspectdb.py b/django/core/management/commands/inspectdb.py
index e44edcfe91..06adfe39ed 100644
--- a/django/core/management/commands/inspectdb.py
+++ b/django/core/management/commands/inspectdb.py
@@ -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 DecimalFields with only one of max_digits and
+ # decimal_fields specified. This configuration would be incorrect,
+ # but nothing more correct 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
diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py
index 2ada5177be..ecc283ff6b 100644
--- a/django/db/backends/base/features.py
+++ b/django/db/backends/base/features.py
@@ -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
diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py
index 48fa01ebee..9114777030 100644
--- a/django/db/backends/oracle/base.py
+++ b/django/db/backends/oracle/base.py
@@ -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)",
diff --git a/django/db/backends/oracle/features.py b/django/db/backends/oracle/features.py
index c07d9f1ed0..dd356181e3 100644
--- a/django/db/backends/oracle/features.py
+++ b/django/db/backends/oracle/features.py
@@ -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).
diff --git a/django/db/backends/oracle/introspection.py b/django/db/backends/oracle/introspection.py
index 6a0947f8ab..07b7cad840 100644
--- a/django/db/backends/oracle/introspection.py
+++ b/django/db/backends/oracle/introspection.py
@@ -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,
diff --git a/django/db/backends/postgresql/base.py b/django/db/backends/postgresql/base.py
index e2728afea5..42b37ab3c2 100644
--- a/django/db/backends/postgresql/base.py
+++ b/django/db/backends/postgresql/base.py
@@ -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)",
diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py
index 5bbf4b86cb..5e4ed320be 100644
--- a/django/db/backends/postgresql/features.py
+++ b/django/db/backends/postgresql/features.py
@@ -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",
diff --git a/django/db/backends/postgresql/introspection.py b/django/db/backends/postgresql/introspection.py
index 791d729ea4..69dd776aa3 100644
--- a/django/db/backends/postgresql/introspection.py
+++ b/django/db/backends/postgresql/introspection.py
@@ -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
diff --git a/django/db/backends/sqlite3/features.py b/django/db/backends/sqlite3/features.py
index 4fa6ab831b..09a0b7fc22 100644
--- a/django/db/backends/sqlite3/features.py
+++ b/django/db/backends/sqlite3/features.py
@@ -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
diff --git a/django/db/models/fields/__init__.py b/django/db/models/fields/__init__.py
index 3e2258e064..27d5657078 100644
--- a/django/db/models/fields/__init__.py
+++ b/django/db/models/fields/__init__.py
@@ -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
+ and "supports_no_precision_decimalfield"
+ not 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
+ and "supports_no_precision_decimalfield"
+ not 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(
diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt
index c297938f45..1a79023b89 100644
--- a/docs/ref/checks.txt
+++ b/docs/ref/checks.txt
@@ -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
diff --git a/docs/ref/models/fields.txt b/docs/ref/models/fields.txt
index 649073b708..9aacbd2922 100644
--- a/docs/ref/models/fields.txt
+++ b/docs/ref/models/fields.txt
@@ -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``
-----------------
diff --git a/docs/releases/6.1.txt b/docs/releases/6.1.txt
index 8c5594d5b6..425419529b 100644
--- a/docs/releases/6.1.txt
+++ b/docs/releases/6.1.txt
@@ -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
~~~~~~~~~~
diff --git a/tests/inspectdb/models.py b/tests/inspectdb/models.py
index 3d6388a7be..fbe1df8a95 100644
--- a/tests/inspectdb/models.py
+++ b/tests/inspectdb/models.py
@@ -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)
diff --git a/tests/inspectdb/tests.py b/tests/inspectdb/tests.py
index 9104671b4f..c16258b0eb 100644
--- a/tests/inspectdb/tests.py
+++ b/tests/inspectdb/tests.py
@@ -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(
diff --git a/tests/invalid_models_tests/test_ordinary_fields.py b/tests/invalid_models_tests/test_ordinary_fields.py
index 1fcf3f708d..2c2653a538 100644
--- a/tests/invalid_models_tests/test_ordinary_fields.py
+++ b/tests/invalid_models_tests/test_ordinary_fields.py
@@ -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",
+ ),
],
)