Skip to content

Commit 1ce6e78

Browse files
authored
Fixed #24920 -- Added support for DecimalField with no precision.
Thanks Lily for the review.
1 parent 5c60763 commit 1ce6e78

16 files changed

Lines changed: 205 additions & 78 deletions

File tree

django/core/management/commands/inspectdb.py

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -359,21 +359,15 @@ def get_field_type(self, connection, table_name, row):
359359
if field_type in {"CharField", "TextField"} and row.collation:
360360
field_params["db_collation"] = row.collation
361361

362-
if field_type == "DecimalField":
363-
if row.precision is None or row.scale is None:
364-
field_notes.append(
365-
"max_digits and decimal_places have been guessed, as this "
366-
"database handles decimal fields as float"
367-
)
368-
field_params["max_digits"] = (
369-
row.precision if row.precision is not None else 10
370-
)
371-
field_params["decimal_places"] = (
372-
row.scale if row.scale is not None else 5
373-
)
374-
else:
375-
field_params["max_digits"] = row.precision
376-
field_params["decimal_places"] = row.scale
362+
if field_type == "DecimalField" and (
363+
# This can generate DecimalFields with only one of max_digits and
364+
# decimal_fields specified. This configuration would be incorrect,
365+
# but nothing more correct could be generated.
366+
row.precision is not None
367+
or row.scale is not None
368+
):
369+
field_params["max_digits"] = row.precision
370+
field_params["decimal_places"] = row.scale
377371

378372
return field_type, field_params, field_notes
379373

django/db/backends/base/features.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,9 @@ class BaseDatabaseFeatures:
383383
# Does the backend support unlimited character columns?
384384
supports_unlimited_charfield = False
385385

386+
# Does the backend support numeric columns with no precision?
387+
supports_no_precision_decimalfield = False
388+
386389
# Does the backend support native tuple lookups (=, >, <, IN)?
387390
supports_tuple_lookups = True
388391

django/db/backends/oracle/base.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ def __get__(self, instance, cls=None):
106106
return instance.__dict__["operators"]
107107

108108

109+
def _get_decimal_column(data):
110+
if data["max_digits"] is None and data["decimal_places"] is None:
111+
return "NUMBER"
112+
return "NUMBER(%(max_digits)s, %(decimal_places)s)" % data
113+
114+
109115
class DatabaseWrapper(BaseDatabaseWrapper):
110116
vendor = "oracle"
111117
display_name = "Oracle"
@@ -125,7 +131,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
125131
"CharField": "NVARCHAR2(%(max_length)s)",
126132
"DateField": "DATE",
127133
"DateTimeField": "TIMESTAMP",
128-
"DecimalField": "NUMBER(%(max_digits)s, %(decimal_places)s)",
134+
"DecimalField": _get_decimal_column,
129135
"DurationField": "INTERVAL DAY(9) TO SECOND(6)",
130136
"FileField": "NVARCHAR2(%(max_length)s)",
131137
"FilePathField": "NVARCHAR2(%(max_length)s)",

django/db/backends/oracle/features.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
7979
supports_json_negative_indexing = False
8080
supports_collation_on_textfield = False
8181
supports_on_delete_db_default = False
82+
supports_no_precision_decimalfield = True
8283
test_now_utc_template = "CURRENT_TIMESTAMP AT TIME ZONE 'UTC'"
8384
django_test_expected_failures = {
8485
# A bug in Django/oracledb with respect to string handling (#23843).

django/db/backends/oracle/introspection.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,14 +194,21 @@ def get_table_description(self, cursor, table_name):
194194
comment,
195195
) = field_map[name]
196196
name %= {} # oracledb, for some reason, doubles percent signs.
197+
if desc[1] == oracledb.NUMBER and desc[5] == -127 and desc[4] == 0:
198+
# DecimalField with no precision.
199+
precision = None
200+
scale = None
201+
else:
202+
precision = desc[4] or 0
203+
scale = desc[5] or 0
197204
description.append(
198205
FieldInfo(
199206
self.identifier_converter(name),
200207
desc[1],
201208
display_size,
202209
desc[3],
203-
desc[4] or 0,
204-
desc[5] or 0,
210+
precision,
211+
scale,
205212
*desc[6:],
206213
default,
207214
collation,

django/db/backends/postgresql/base.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ def _get_varchar_column(data):
8989
return "varchar(%(max_length)s)" % data
9090

9191

92+
def _get_decimal_column(data):
93+
if data["max_digits"] is None and data["decimal_places"] is None:
94+
return "numeric"
95+
return "numeric(%(max_digits)s, %(decimal_places)s)" % data
96+
97+
9298
class DatabaseWrapper(BaseDatabaseWrapper):
9399
vendor = "postgresql"
94100
display_name = "PostgreSQL"
@@ -105,7 +111,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
105111
"CharField": _get_varchar_column,
106112
"DateField": "date",
107113
"DateTimeField": "timestamp with time zone",
108-
"DecimalField": "numeric(%(max_digits)s, %(decimal_places)s)",
114+
"DecimalField": _get_decimal_column,
109115
"DurationField": "interval",
110116
"FileField": "varchar(%(max_length)s)",
111117
"FilePathField": "varchar(%(max_length)s)",

django/db/backends/postgresql/features.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
6868
supports_covering_indexes = True
6969
supports_stored_generated_columns = True
7070
supports_nulls_distinct_unique_constraints = True
71+
supports_no_precision_decimalfield = True
7172
can_rename_index = True
7273
test_collations = {
7374
"deterministic": "C",

django/db/backends/postgresql/introspection.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,10 @@ def get_table_description(self, cursor, table_name):
137137
# display_size is always None on psycopg2.
138138
line.internal_size if line.display_size is None else line.display_size,
139139
line.internal_size,
140-
line.precision,
141-
line.scale,
140+
# precision and scale are always 2^16 - 1 on psycopg2 for
141+
# DecimalFields with no precision.
142+
None if line.precision == 2**16 - 1 else line.precision,
143+
None if line.scale == 2**16 - 1 else line.scale,
142144
*field_map[line.name],
143145
)
144146
for line in cursor.description

django/db/backends/sqlite3/features.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
5555
insert_test_table_with_defaults = 'INSERT INTO {} ("null") VALUES (1)'
5656
supports_default_keyword_in_insert = False
5757
supports_unlimited_charfield = True
58+
supports_no_precision_decimalfield = True
5859
can_return_columns_from_insert = True
5960
can_return_rows_from_bulk_insert = True
6061
can_return_rows_from_update = True

django/db/models/fields/__init__.py

Lines changed: 72 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,54 +1725,84 @@ def check(self, **kwargs):
17251725
return errors
17261726

17271727
def _check_decimal_places(self):
1728-
try:
1729-
decimal_places = int(self.decimal_places)
1730-
if decimal_places < 0:
1731-
raise ValueError()
1732-
except TypeError:
1733-
return [
1734-
checks.Error(
1735-
"DecimalFields must define a 'decimal_places' attribute.",
1736-
obj=self,
1737-
id="fields.E130",
1738-
)
1739-
]
1740-
except ValueError:
1741-
return [
1742-
checks.Error(
1743-
"'decimal_places' must be a non-negative integer.",
1744-
obj=self,
1745-
id="fields.E131",
1746-
)
1747-
]
1728+
if self.decimal_places is None:
1729+
if (
1730+
not connection.features.supports_no_precision_decimalfield
1731+
and "supports_no_precision_decimalfield"
1732+
not in self.model._meta.required_db_features
1733+
):
1734+
return [
1735+
checks.Error(
1736+
"DecimalFields must define a 'decimal_places' attribute.",
1737+
obj=self,
1738+
id="fields.E130",
1739+
)
1740+
]
1741+
elif self.max_digits is not None:
1742+
return [
1743+
checks.Error(
1744+
"DecimalField’s max_digits and decimal_places must both "
1745+
"be defined or both omitted.",
1746+
obj=self,
1747+
id="fields.E135",
1748+
),
1749+
]
17481750
else:
1749-
return []
1751+
try:
1752+
decimal_places = int(self.decimal_places)
1753+
if decimal_places < 0:
1754+
raise ValueError()
1755+
except ValueError:
1756+
return [
1757+
checks.Error(
1758+
"'decimal_places' must be a non-negative integer.",
1759+
obj=self,
1760+
id="fields.E131",
1761+
)
1762+
]
1763+
return []
17501764

17511765
def _check_max_digits(self):
1752-
try:
1753-
max_digits = int(self.max_digits)
1754-
if max_digits <= 0:
1755-
raise ValueError()
1756-
except TypeError:
1757-
return [
1758-
checks.Error(
1759-
"DecimalFields must define a 'max_digits' attribute.",
1760-
obj=self,
1761-
id="fields.E132",
1762-
)
1763-
]
1764-
except ValueError:
1765-
return [
1766-
checks.Error(
1767-
"'max_digits' must be a positive integer.",
1768-
obj=self,
1769-
id="fields.E133",
1770-
)
1771-
]
1766+
if self.max_digits is None:
1767+
if (
1768+
not connection.features.supports_no_precision_decimalfield
1769+
and "supports_no_precision_decimalfield"
1770+
not in self.model._meta.required_db_features
1771+
):
1772+
return [
1773+
checks.Error(
1774+
"DecimalFields must define a 'max_digits' attribute.",
1775+
obj=self,
1776+
id="fields.E132",
1777+
)
1778+
]
1779+
elif self.decimal_places is not None:
1780+
return [
1781+
checks.Error(
1782+
"DecimalField’s max_digits and decimal_places must both "
1783+
"be defined or both omitted.",
1784+
obj=self,
1785+
id="fields.E135",
1786+
),
1787+
]
17721788
else:
1773-
return []
1789+
try:
1790+
max_digits = int(self.max_digits)
1791+
if max_digits <= 0:
1792+
raise ValueError()
1793+
except ValueError:
1794+
return [
1795+
checks.Error(
1796+
"'max_digits' must be a positive integer.",
1797+
obj=self,
1798+
id="fields.E133",
1799+
)
1800+
]
1801+
return []
17741802

17751803
def _check_decimal_places_and_max_digits(self, **kwargs):
1804+
if self.decimal_places is None or self.max_digits is None:
1805+
return []
17761806
if int(self.decimal_places) > int(self.max_digits):
17771807
return [
17781808
checks.Error(

0 commit comments

Comments
 (0)