Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 9 additions & 15 deletions django/core/management/commands/inspectdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,21 +359,15 @@ def get_field_type(self, connection, table_name, row):
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

Expand Down
3 changes: 3 additions & 0 deletions django/db/backends/base/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 7 additions & 1 deletion django/db/backends/oracle/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,12 @@ def __get__(self, instance, cls=None):
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"
Expand All @@ -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)",
Expand Down
1 change: 1 addition & 0 deletions django/db/backends/oracle/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
11 changes: 9 additions & 2 deletions django/db/backends/oracle/introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,14 +194,21 @@ def get_table_description(self, cursor, table_name):
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,
Expand Down
8 changes: 7 additions & 1 deletion django/db/backends/postgresql/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)",
Expand Down
1 change: 1 addition & 0 deletions django/db/backends/postgresql/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 4 additions & 2 deletions django/db/backends/postgresql/introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,10 @@ def get_table_description(self, cursor, table_name):
# 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
Expand Down
1 change: 1 addition & 0 deletions django/db/backends/sqlite3/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
114 changes: 72 additions & 42 deletions django/db/models/fields/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1725,54 +1725,84 @@ def check(self, **kwargs):
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(
Expand Down
2 changes: 2 additions & 0 deletions docs/ref/checks.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 14 additions & 3 deletions docs/ref/models/fields.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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::
Expand All @@ -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``
-----------------

Expand Down
5 changes: 5 additions & 0 deletions docs/releases/6.1.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~

Expand Down
9 changes: 9 additions & 0 deletions tests/inspectdb/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,15 @@ class Meta:
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)
Expand Down
16 changes: 9 additions & 7 deletions tests/inspectdb/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,13 @@ def test_char_field_unlimited(self):
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()
Expand All @@ -228,13 +235,8 @@ def test_number_field_types(self):
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(
Expand Down
Loading