diff options
| author | Luke Plant <L.Plant.98@cantab.net> | 2021-12-29 14:00:50 +0000 |
|---|---|---|
| committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2022-03-31 11:05:23 +0200 |
| commit | 40b8a6174f001a310aa33f7880db0efeeb04d4c4 (patch) | |
| tree | 2a4a940f81dbab0fb3723b1d4de9d4db04f9894e /django | |
| parent | 1efea11808f7b8a3b31445e0c1c7d270f832d965 (diff) | |
Fixed #33397 -- Corrected resolving output_field for DateField/DateTimeField/TimeField/DurationFields.
This includes refactoring of CombinedExpression._resolve_output_field()
so it no longer uses the behavior inherited from Expression of guessing
same output type if argument types match, and instead we explicitly
define the output type of all supported operations.
This also makes nonsensical operations involving dates
(e.g. date + date) raise a FieldError, and adds support for
automatically inferring output_field for cases such as:
* date - date
* date + duration
* date - duration
* time + duration
* time - time
Diffstat (limited to 'django')
| -rw-r--r-- | django/contrib/postgres/search.py | 7 | ||||
| -rw-r--r-- | django/db/models/expressions.py | 105 |
2 files changed, 95 insertions, 17 deletions
diff --git a/django/contrib/postgres/search.py b/django/contrib/postgres/search.py index f652c1d346..d43163a40b 100644 --- a/django/contrib/postgres/search.py +++ b/django/contrib/postgres/search.py @@ -10,7 +10,7 @@ from django.db.models import ( TextField, Value, ) -from django.db.models.expressions import CombinedExpression +from django.db.models.expressions import CombinedExpression, register_combinable_fields from django.db.models.functions import Cast, Coalesce @@ -79,6 +79,11 @@ class SearchVectorCombinable: return CombinedSearchVector(self, connector, other, self.config) +register_combinable_fields( + SearchVectorField, SearchVectorCombinable.ADD, SearchVectorField, SearchVectorField +) + + class SearchVector(SearchVectorCombinable, Func): function = "to_tsvector" arg_joiner = " || ' ' || " diff --git a/django/db/models/expressions.py b/django/db/models/expressions.py index 32982777ef..add7b20ba3 100644 --- a/django/db/models/expressions.py +++ b/django/db/models/expressions.py @@ -310,18 +310,18 @@ class BaseExpression: def _resolve_output_field(self): """ - Attempt to infer the output type of the expression. If the output - fields of all source fields match then, simply infer the same type - here. This isn't always correct, but it makes sense most of the time. + Attempt to infer the output type of the expression. - Consider the difference between `2 + 2` and `2 / 3`. Inferring - the type here is a convenience for the common case. The user should - supply their own output_field with more complex computations. + As a guess, if the output fields of all source fields match then simply + infer the same type here. If a source's output field resolves to None, exclude it from this check. If all sources are None, then an error is raised higher up the stack in the output_field property. """ + # This guess is mostly a bad idea, but there is quite a lot of code + # (especially 3rd party Func subclasses) that depend on it, we'd need a + # deprecation path to fix it. sources_iter = ( source for source in self.get_source_fields() if source is not None ) @@ -467,6 +467,13 @@ class Expression(BaseExpression, Combinable): # Type inference for CombinedExpression.output_field. +# Missing items will result in FieldError, by design. +# +# The current approach for NULL is based on lowest common denominator behavior +# i.e. if one of the supported databases is raising an error (rather than +# return NULL) for `val <op> NULL`, then Django raises FieldError. +NoneType = type(None) + _connector_combinations = [ # Numeric operations - operands of same type. { @@ -482,6 +489,8 @@ _connector_combinations = [ # Behavior for DIV with integer arguments follows Postgres/SQLite, # not MySQL/Oracle. Combinable.DIV, + Combinable.MOD, + Combinable.POW, ) }, # Numeric operations - operands of different type. @@ -499,6 +508,66 @@ _connector_combinations = [ Combinable.DIV, ) }, + # Bitwise operators. + { + connector: [ + (fields.IntegerField, fields.IntegerField, fields.IntegerField), + ] + for connector in ( + Combinable.BITAND, + Combinable.BITOR, + Combinable.BITLEFTSHIFT, + Combinable.BITRIGHTSHIFT, + Combinable.BITXOR, + ) + }, + # Numeric with NULL. + { + connector: [ + (field_type, NoneType, field_type), + (NoneType, field_type, field_type), + ] + for connector in ( + Combinable.ADD, + Combinable.SUB, + Combinable.MUL, + Combinable.DIV, + Combinable.MOD, + Combinable.POW, + ) + for field_type in (fields.IntegerField, fields.DecimalField, fields.FloatField) + }, + # Date/DateTimeField/DurationField/TimeField. + { + Combinable.ADD: [ + # Date/DateTimeField. + (fields.DateField, fields.DurationField, fields.DateTimeField), + (fields.DateTimeField, fields.DurationField, fields.DateTimeField), + (fields.DurationField, fields.DateField, fields.DateTimeField), + (fields.DurationField, fields.DateTimeField, fields.DateTimeField), + # DurationField. + (fields.DurationField, fields.DurationField, fields.DurationField), + # TimeField. + (fields.TimeField, fields.DurationField, fields.TimeField), + (fields.DurationField, fields.TimeField, fields.TimeField), + ], + }, + { + Combinable.SUB: [ + # Date/DateTimeField. + (fields.DateField, fields.DurationField, fields.DateTimeField), + (fields.DateTimeField, fields.DurationField, fields.DateTimeField), + (fields.DateField, fields.DateField, fields.DurationField), + (fields.DateField, fields.DateTimeField, fields.DurationField), + (fields.DateTimeField, fields.DateField, fields.DurationField), + (fields.DateTimeField, fields.DateTimeField, fields.DurationField), + # DurationField. + (fields.DurationField, fields.DurationField, fields.DurationField), + # TimeField. + (fields.TimeField, fields.DurationField, fields.TimeField), + (fields.TimeField, fields.TimeField, fields.DurationField), + ], + }, ] _connector_combinators = defaultdict(list) @@ -552,17 +621,21 @@ class CombinedExpression(SQLiteNumericMixin, Expression): self.lhs, self.rhs = exprs def _resolve_output_field(self): - try: - return super()._resolve_output_field() - except FieldError: - combined_type = _resolve_combined_type( - self.connector, - type(self.lhs.output_field), - type(self.rhs.output_field), + # We avoid using super() here for reasons given in + # Expression._resolve_output_field() + combined_type = _resolve_combined_type( + self.connector, + type(self.lhs._output_field_or_none), + type(self.rhs._output_field_or_none), + ) + if combined_type is None: + raise FieldError( + f"Cannot infer type of {self.connector!r} expression involving these " + f"types: {self.lhs.output_field.__class__.__name__}, " + f"{self.rhs.output_field.__class__.__name__}. You must set " + f"output_field." ) - if combined_type is None: - raise - return combined_type() + return combined_type() def as_sql(self, compiler, connection): expressions = [] |
