Opened 3 weeks ago

Last modified 41 hours ago

#37330 assigned Bug

Transform on an annotation alias crashes when output_field is not attached to a model

Reported by: Kunal Kumar Owned by: Kunal Kumar
Component: Database layer (models, ORM) Version: dev
Severity: Release blocker Keywords:
Cc: Jacob Walls Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: yes Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Applying a transform to an annotation alias raises AttributeError when the
annotation's output_field is not attached to a model:

Book.objects.annotate(
    published=ExpressionWrapper(F("pubdate"), output_field=DateField())
).values("published__year")
AttributeError: 'DateField' object has no attribute 'model'

Value(..., output_field=DateField()) fails the same way.

The same query works when the annotation's output_field happens to be a model
field, so the outcome depends on how the annotation was spelled rather than on
what it asks for:

annotation .values("ayear")
annotate(a=F("pubdate")) works
annotate(a=ExpressionWrapper(F("pubdate"), output_field=DateField())) AttributeError
annotate(a=Value(date(2026, 1, 1), output_field=DateField())) AttributeError

Cause: names_to_path() resolves an annotation name to
self.annotations[name].output_field, which annotate() does not require to be
attached to a model. setup_joins()'s final_transformer() then calls
field.get_col(alias), and Field.get_col() dereferences self.model.

names_to_path() already anticipates this a few lines earlier:

try:
    model = field.model._meta.concrete_model
except AttributeError:
    # QuerySet.annotate() may introduce fields that aren't attached to a model.
    model = None

An AttributeError escaping here also bypasses add_fields(), which catches
FieldError to build a helpful message.

Closest precedent found: #34921, same class of failure (an unsuccessful
self.model access on an unbound field, there in DateTimeField.to_python()).
I also searched for "unbound annotation", "output_field model attribute" and
"has no attribute model"; #36787 and #37248 are the nearest hits and neither
covers this path — both are closed/fixed with different symptoms.

Patch: ​https://github.com/django/django/pull/21914

Change History (5)

comment:1 by Kunal Kumar, 3 weeks ago

Component: Uncategorized → Database layer (models, ORM)
Owner: set to Kunal Kumar

comment:2 by Sarah Boyce, 3 weeks ago

Triage Stage: Unreviewed → Accepted

Replicated, thank you for the report

comment:3 by Sarah Boyce, 3 weeks ago

Patch needs improvement: set

comment:4 by David Smith, 4 days ago

Cc: Jacob Walls added
Severity: Normal → Release blocker

The same query works when the annotation's output_field happens to be a model field

While it doesn't crash, the output seems incorrect to me.

Here is a test to add to tests/annotations/tests.py

    @register_lookup(CharField, Length)
    def test_values_transform_on_annotation_of_related_field(self):
        # Book and Publisher both have a "name" column.
        qs = Book.objects.annotate(publisher_name=F("publisher__name"))
        self.assertEqual(
            qs.values_list("publisher_name__length", flat=True).get(pk=self.b1.pk),
            len(self.p1.name),
        )

This fails with

AssertionError: 58 != 6

The query returned the length of the book name rather than the publisher name.

I bisected the change to be6cf832293779d8aeaeddca8c47a37ba1530898. Before this change it crashed with FieldError but now returns a value which I believe to be incorrect. I think the severity of this should be increased to a release blocker.

comment:5 by Jacob Walls, 41 hours ago

Needs tests: set
Patch needs improvement: unset

Good catch, David. Kunal, can you add David's test case to your PR?

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