Error when annotating distance between 2 points inside a Subquery - GeoDjango

Hi. I am on Django 3.2 using Python 3.8 and Postgres.

I’m trying to annotate the distance between a user and facility. My minimal code is as follows:

User.objects.all().annotate(
    test=Subquery(
        Facility.objects.annotate(
            distance=Distance("facility_address__pnt", OuterRef("user_address__pnt"))
        ).values("distance")[:1]
    )
)

The User and Facility models both have relations to the user_address and facility_address relations respectively. These both have a point attribute which I am attempting to get the distance between.

However this raises an AttributeError which I’ve posted in the stack trace below. I haven’t found anything in the docs that says I shouldn’t be able to do this. Is there something incompatible here with the Geographic functions? Or am I overlooking something simple?

Also, I understand it’s a stupid example, but the query I need this for is a lot more complex.

File ~/xxx/venv/lib/python3.8/site-packages/django/db/models/manager.py:85, in BaseManager._get_queryset_methods.<locals>.create_method.<locals>.manager_method(self, *args, **kwargs)
     84 def manager_method(self, *args, **kwargs):
---> 85     return getattr(self.get_queryset(), name)(*args, **kwargs)

File ~/xxx/venv/lib/python3.8/site-packages/django/db/models/query.py:1091, in QuerySet.annotate(self, *args, **kwargs)
   1086 """
   1087 Return a query set in which the returned objects have been annotated
   1088 with extra data or aggregations.
   1089 """
   1090 self._not_support_combined_queries('annotate')
-> 1091 return self._annotate(args, kwargs, select=True)

File ~/xxx/venv/lib/python3.8/site-packages/django/db/models/query.py:1130, in QuerySet._annotate(self, args, kwargs, select)
   1128         clone.query.add_filtered_relation(annotation, alias)
   1129     else:
-> 1130         clone.query.add_annotation(
   1131             annotation, alias, is_summary=False, select=select,
   1132         )
   1133 for alias, annotation in clone.query.annotations.items():
   1134     if alias in annotations and annotation.contains_aggregate:

File ~/xxx/venv/lib/python3.8/site-packages/django/db/models/sql/query.py:1062, in Query.add_annotation(self, annotation, alias, is_summary, select)
   1060 """Add a single annotation expression to the Query."""
   1061 self.check_alias(alias)
-> 1062 annotation = annotation.resolve_expression(self, allow_joins=True, reuse=None,
   1063                                            summarize=is_summary)
   1064 if select:
   1065     self.append_annotation_mask([alias])

File ~/xxx/venv/lib/python3.8/site-packages/django/contrib/gis/db/models/functions.py:59, in GeoFuncMixin.resolve_expression(self, *args, **kwargs)
     56 res = super().resolve_expression(*args, **kwargs)
     58 # Ensure that expressions are geometric.
---> 59 source_fields = res.get_source_fields()
     60 for pos in self.geom_param_pos:
     61     field = source_fields[pos]

File ~/xxx/venv/lib/python3.8/site-packages/django/db/models/expressions.py:358, in BaseExpression.get_source_fields(self)
    356 def get_source_fields(self):
    357     """Return the underlying field types used by this aggregate."""
--> 358     return [e._output_field_or_none for e in self.get_source_expressions()]

File ~/xxx/venv/lib/python3.8/site-packages/django/db/models/expressions.py:358, in <listcomp>(.0)
    356 def get_source_fields(self):
    357     """Return the underlying field types used by this aggregate."""
--> 358     return [e._output_field_or_none for e in self.get_source_expressions()]

AttributeError: 'ResolvedOuterRef' object has no attribute '_output_field_or_none'

This expression:

would be a references to the outer query for User. Does your User model have a user_address field? What is the type of that field? (Superficially, I would think it would need to be either a ForeignKey or a OneToOneField to a different model, that model having a pnt field being a Point.

I think you might have to wrap the OuterRef in ExpressionWrapper here to tell Distance that it will eventually resolve to a PointField

User.objects.annotate(
    test=Facility.objects.annotate(
        distance=Distance(
            "facility_address__pnt",
            ExpressionWrapper(
                OuterRef("user_address__pnt"),
                output_field=PointField(),
            ),
        ),
    ).values("distance")[:1]
)
1 Like

Wow, thanks a million! Not sure I would have thought of that!

For the record, this is tracked in this ticket.

I appreciate you pushing that forward for me.

Just for my own understanding, the issue is that since I’m using the OuterRef inside a database function, the function is attempting to check the value of OuterRef before it’s fully resolved? How is that different than doing some kind of simple filtering on the OuterRef such as User.objects.all().annotate(test=Subquery(Facility.objects.filter(user_id=OuterRef("pk")).values("name")[1:]))?

The different between the two is that filter(user_id=OuterRef("pk")) translates to lookups.Exact(F("user_id"), OuterRef("pk")) which has a pre-defined output_field = BooleanField().

In the case of the Distance function it has some specialized logic to ensure that each expressions are geometric that kicks in the moment the filter call is made. The problem is that at this point the OuterRef has no way of knowing it will be used in the context of another queryset and cannot resolve its output field.

1 Like