Opened 3 years ago

Closed 3 years ago

Last modified 3 years ago

#34745 closed Bug (invalid)

Arbitrary rhs of lookup gives "Object has no attribute 'contains_aggregate'"

Reported by: Jacob Walls Owned by: nobody
Component: Database layer (models, ORM) Version: dev
Severity: Normal Keywords:
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

This example overrides JSONField.get_prep_value to return some arbitrary SQL. Later, a lookup raises an AttributeError.

It also includes two commented out workarounds. I don't know if this usage is supported, but I bisected the behavior change to f42ccdd835e5b3f0914b5e6f87621c648136ea36, which no longer uses getattr() to silence AttributeError for right-hand sides. I found this upgrading a ​3.2 project to 4.0.


If this usage isn't supported, what is the recommended interface to implement when writing a custom as_sql() method?

models.py:

from django.db import models
from django.db.models.fields.json import JSONField
from django.db.models.sql.where import NothingNode

class ArbitrarySQL:  # class ArbitrarySQL(NothingNode):
    # contains_aggregate = False
    def as_sql(self, compiler, connection):
        return "'{}'", ()


class MyJSONField(JSONField):
    def get_prep_value(self, value):
        return ArbitrarySQL()


class MyModel(models.Model):
    config = MyJSONField()

Gives:

>>> from repro_app.models import MyModel
>>> MyModel.objects.filter(config={})
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/Users/jwalls/django/django/db/models/query.py", line 373, in __repr__
    data = list(self[: REPR_OUTPUT_SIZE + 1])
  File "/Users/jwalls/django/django/db/models/query.py", line 397, in __iter__
    self._fetch_all()
  File "/Users/jwalls/django/django/db/models/query.py", line 1897, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
  File "/Users/jwalls/django/django/db/models/query.py", line 90, in __iter__
    results = compiler.execute_sql(
  File "/Users/jwalls/django/django/db/models/sql/compiler.py", line 1549, in execute_sql
    sql, params = self.as_sql()
  File "/Users/jwalls/django/django/db/models/sql/compiler.py", line 736, in as_sql
    extra_select, order_by, group_by = self.pre_sql_setup(
  File "/Users/jwalls/django/django/db/models/sql/compiler.py", line 86, in pre_sql_setup
    self.where, self.having, self.qualify = self.query.where.split_having_qualify(
  File "/Users/jwalls/django/django/db/models/sql/where.py", line 46, in split_having_qualify
    if not self.contains_aggregate and not self.contains_over_clause:
  File "/Users/jwalls/django/django/utils/functional.py", line 47, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File "/Users/jwalls/django/django/db/models/sql/where.py", line 249, in contains_aggregate
    return self._contains_aggregate(self)
  File "/Users/jwalls/django/django/db/models/sql/where.py", line 244, in _contains_aggregate
    return any(cls._contains_aggregate(c) for c in obj.children)
  File "/Users/jwalls/django/django/db/models/sql/where.py", line 244, in <genexpr>
    return any(cls._contains_aggregate(c) for c in obj.children)
  File "/Users/jwalls/django/django/db/models/sql/where.py", line 245, in _contains_aggregate
    return obj.contains_aggregate
  File "/Users/jwalls/django/django/utils/functional.py", line 47, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File "/Users/jwalls/django/django/db/models/expressions.py", line 242, in contains_aggregate
    return any(
  File "/Users/jwalls/django/django/db/models/expressions.py", line 243, in <genexpr>
    expr and expr.contains_aggregate for expr in self.get_source_expressions()
AttributeError: 'ArbitrarySQL' object has no attribute 'contains_aggregate'

Change History (1)

comment:1 by Simon Charette, 3 years ago

Resolution: → invalid
Status: new → closed

I don't think we offer any guarantees that an object that implements the compilable protocol (as_sql(compiler, connection) -> tuple[str, tuple]) is enough to be passed around the ORM. Not against it but it warrants a larger discussion and would likely require the introduction of typing to the ORM to enforce properly through continuous integration.

If you want to return arbitrary SQL I'd suggest using ​RawSQL directly or subclass it as it implements most of the implicit attributes the ORM expects.

Last edited 3 years ago by Simon Charette (previous) (diff)
Note: See TracTickets for help on using tickets.
Back to Top