Opened 12 months ago

Closed 12 months ago

Last modified 12 months ago

#34458 closed Bug (fixed)

QuerySet.defer('user_id') raises AttributeError: 'ForeignKey' object has no attribute 'field'

Reported by: Andrew Cordery Owned by: Simon Charette
Component: Database layer (models, ORM) Version: 4.2
Severity: Release blocker Keywords:
Cc: Simon Charette Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Calling defer() with the column name of a foreign key ('user_id') instead of the field name ('user') will raise an AttributeError: 'ForeignKey' object has no attribute 'field'

I believe this is either a regression in 4.2 or an undocumented change of behavior. Generally speaking, I'd prefer this new requirement to always use the field name as it reduces ambiguity/improves refactoring etc. However, it needs documentation and a better error message.

Here is an example traceback:

In [11]: User.objects.defer('organization_id')
Out[11]: ---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
File ~/.local/share/virtualenvs/axi-v3J2FyDc/lib/python3.11/site-packages/IPython/core/formatters.py:708, in PlainTextFormatter.__call__(self, obj)
    701 stream = StringIO()
    702 printer = pretty.RepresentationPrinter(stream, self.verbose,
    703     self.max_width, self.newline,
    704     max_seq_length=self.max_seq_length,
    705     singleton_pprinters=self.singleton_printers,
    706     type_pprinters=self.type_printers,
    707     deferred_pprinters=self.deferred_printers)
--> 708 printer.pretty(obj)
    709 printer.flush()
    710 return stream.getvalue()

File ~/.local/share/virtualenvs/axi-v3J2FyDc/lib/python3.11/site-packages/IPython/lib/pretty.py:410, in RepresentationPrinter.pretty(self, obj)
    407                         return meth(obj, self, cycle)
    408                 if cls is not object \
    409                         and callable(cls.__dict__.get('__repr__')):
--> 410                     return _repr_pprint(obj, self, cycle)
    412     return _default_pprint(obj, self, cycle)
    413 finally:

File ~/.local/share/virtualenvs/axi-v3J2FyDc/lib/python3.11/site-packages/IPython/lib/pretty.py:778, in _repr_pprint(obj, p, cycle)
    776 """A pprint that just redirects to the normal repr function."""
    777 # Find newlines and replace them with p.break_()
--> 778 output = repr(obj)
    779 lines = output.splitlines()
    780 with p.group():

File ~/.local/share/virtualenvs/axi-v3J2FyDc/lib/python3.11/site-packages/django/db/models/query.py:374, in QuerySet.__repr__(self)
    373 def __repr__(self):
--> 374     data = list(self[: REPR_OUTPUT_SIZE + 1])
    375     if len(data) > REPR_OUTPUT_SIZE:
    376         data[-1] = "...(remaining elements truncated)..."

File ~/.local/share/virtualenvs/axi-v3J2FyDc/lib/python3.11/site-packages/django/db/models/query.py:398, in QuerySet.__iter__(self)
    383 def __iter__(self):
    384     """
    385     The queryset iterator protocol uses three nested iterators in the
    386     default case:
   (...)
    396            - Responsible for turning the rows into model objects.
    397     """
--> 398     self._fetch_all()
    399     return iter(self._result_cache)

File ~/.local/share/virtualenvs/axi-v3J2FyDc/lib/python3.11/site-packages/django/db/models/query.py:1881, in QuerySet._fetch_all(self)
   1879 def _fetch_all(self):
   1880     if self._result_cache is None:
-> 1881         self._result_cache = list(self._iterable_class(self))
   1882     if self._prefetch_related_lookups and not self._prefetch_done:
   1883         self._prefetch_related_objects()

File ~/.local/share/virtualenvs/axi-v3J2FyDc/lib/python3.11/site-packages/django/db/models/query.py:91, in ModelIterable.__iter__(self)
     88 compiler = queryset.query.get_compiler(using=db)
     89 # Execute the query. This will also fill compiler.select, klass_info,
     90 # and annotations.
---> 91 results = compiler.execute_sql(
     92     chunked_fetch=self.chunked_fetch, chunk_size=self.chunk_size
     93 )
     94 select, klass_info, annotation_col_map = (
     95     compiler.select,
     96     compiler.klass_info,
     97     compiler.annotation_col_map,
     98 )
     99 model_cls = klass_info["model"]

File ~/.local/share/virtualenvs/axi-v3J2FyDc/lib/python3.11/site-packages/django/db/models/sql/compiler.py:1547, in SQLCompiler.execute_sql(self, result_type, chunked_fetch, chunk_size)
   1545 result_type = result_type or NO_RESULTS
   1546 try:
-> 1547     sql, params = self.as_sql()
   1548     if not sql:
   1549         raise EmptyResultSet

File ~/.local/share/virtualenvs/axi-v3J2FyDc/lib/python3.11/site-packages/django/db/models/sql/compiler.py:734, in SQLCompiler.as_sql(self, with_limits, with_col_aliases)
    732 try:
    733     combinator = self.query.combinator
--> 734     extra_select, order_by, group_by = self.pre_sql_setup(
    735         with_col_aliases=with_col_aliases or bool(combinator),
    736     )
    737     for_update_part = None
    738     # Is a LIMIT/OFFSET clause needed?

File ~/.local/share/virtualenvs/axi-v3J2FyDc/lib/python3.11/site-packages/django/db/models/sql/compiler.py:84, in SQLCompiler.pre_sql_setup(self, with_col_aliases)
     78 def pre_sql_setup(self, with_col_aliases=False):
     79     """
     80     Do any necessary class setup immediately prior to producing SQL. This
     81     is for things that can't necessarily be done in __init__ because we
     82     might not have all the pieces in place at that time.
     83     """
---> 84     self.setup_query(with_col_aliases=with_col_aliases)
     85     order_by = self.get_order_by()
     86     self.where, self.having, self.qualify = self.query.where.split_having_qualify(
     87         must_group_by=self.query.group_by is not None
     88     )

File ~/.local/share/virtualenvs/axi-v3J2FyDc/lib/python3.11/site-packages/django/db/models/sql/compiler.py:73, in SQLCompiler.setup_query(self, with_col_aliases)
     71 if all(self.query.alias_refcount[a] == 0 for a in self.query.alias_map):
     72     self.query.get_initial_alias()
---> 73 self.select, self.klass_info, self.annotation_col_map = self.get_select(
     74     with_col_aliases=with_col_aliases,
     75 )
     76 self.col_count = len(self.select)

File ~/.local/share/virtualenvs/axi-v3J2FyDc/lib/python3.11/site-packages/django/db/models/sql/compiler.py:256, in SQLCompiler.get_select(self, with_col_aliases)
    254     select_idx += 1
    255 assert not (self.query.select and self.query.default_cols)
--> 256 select_mask = self.query.get_select_mask()
    257 if self.query.default_cols:
    258     cols = self.get_default_columns(select_mask)

File ~/.local/share/virtualenvs/axi-v3J2FyDc/lib/python3.11/site-packages/django/db/models/sql/query.py:768, in Query.get_select_mask(self)
    766 opts = self.get_meta()
    767 if defer:
--> 768     return self._get_defer_select_mask(opts, mask)
    769 return self._get_only_select_mask(opts, mask)

File ~/.local/share/virtualenvs/axi-v3J2FyDc/lib/python3.11/site-packages/django/db/models/sql/query.py:724, in Query._get_defer_select_mask(self, opts, mask, select_mask)
    722     field = relation.field
    723 else:
--> 724     field = opts.get_field(field_name).field
    725     field_select_mask = select_mask.setdefault(field, {})
    726 related_model = field.model._meta.concrete_model

AttributeError: 'ForeignKey' object has no attribute 'field'

Change History (7)

comment:1 by David Sanders, 12 months ago

Triage Stage: UnreviewedAccepted

Thanks for the report 👍

It's at least inconsistent with the behaviour of only() which does allow the column name.

comment:2 by Mariusz Felisiak, 12 months ago

Cc: Simon Charette added
Severity: NormalRelease blocker
Summary: Calling defer('user_id') instead of defer('user') will raise AttributeError: 'ForeignKey' object has no attribute 'field'QuerySet.defer('user_id') raises AttributeError: 'ForeignKey' object has no attribute 'field'

comment:3 by Simon Charette, 12 months ago

Owner: changed from nobody to Simon Charette
Status: newassigned

comment:4 by Simon Charette, 12 months ago

Has patch: set

comment:5 by Mariusz Felisiak, 12 months ago

Triage Stage: AcceptedReady for checkin

comment:6 by Mariusz Felisiak <felisiak.mariusz@…>, 12 months ago

Resolution: fixed
Status: assignedclosed

In 87c63bd:

Fixed #34458 -- Fixed QuerySet.defer() crash on attribute names.

Thanks Andrew Cordery for the report.

Regression in b3db6c8dcb5145f7d45eff517bcd96460475c879.

comment:7 by Mariusz Felisiak <felisiak.mariusz@…>, 12 months ago

In 647920b:

[4.2.x] Fixed #34458 -- Fixed QuerySet.defer() crash on attribute names.

Thanks Andrew Cordery for the report.

Regression in b3db6c8dcb5145f7d45eff517bcd96460475c879.

Backport of 87c63bd8df0eb0109df333df1264c6a96707d6a4 from main

Note: See TracTickets for help on using tickets.
Back to Top