diff options
| author | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2025-11-17 13:43:47 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-17 13:43:47 +0100 |
| commit | 1ce6e78dd4beed702f15fa0be798dd17a15d4ba8 (patch) | |
| tree | 0078da5312fb69897bb6327040d51c3450d2fa3b /django/db | |
| parent | 5c60763561c67924eff1069e1516b60a59d068d5 (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.py | 3 | ||||
| -rw-r--r-- | django/db/backends/oracle/base.py | 8 | ||||
| -rw-r--r-- | django/db/backends/oracle/features.py | 1 | ||||
| -rw-r--r-- | django/db/backends/oracle/introspection.py | 11 | ||||
| -rw-r--r-- | django/db/backends/postgresql/base.py | 8 | ||||
| -rw-r--r-- | django/db/backends/postgresql/features.py | 1 | ||||
| -rw-r--r-- | django/db/backends/postgresql/introspection.py | 6 | ||||
| -rw-r--r-- | django/db/backends/sqlite3/features.py | 1 | ||||
| -rw-r--r-- | django/db/models/fields/__init__.py | 114 |
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( |
