summaryrefslogtreecommitdiff
path: root/django/db
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 /django/db
parent5c60763561c67924eff1069e1516b60a59d068d5 (diff)
Fixed #24920 -- Added support for DecimalField with no precision.
Thanks Lily for the review.
Diffstat (limited to 'django/db')
-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
9 files changed, 105 insertions, 48 deletions
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(