Opened 10 months ago

Closed 10 months ago

Last modified 10 months 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, 10 months ago

Resolution: invalid
Status: newclosed

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 10 months ago by Simon Charette (previous) (diff)
Note: See TracTickets for help on using tickets.
Back to Top