django-mock-queries
django-mock-queries copied to clipboard
Add support for Subquery expressions
Related to #181.
After introducing changes from #181 to properly mock MockSet.query, the following breaks because utils.get_attribute doesn't currently handle Subquery expressions.
ms = MockSet()
ms.annotate(
my_field=Subquery(ms.filter(value=OuterRef("value")))
)
# Raises AttributeError: Mock object has no attribute 'split'
I've been playing around the codebase, trying to see if I could fit this in, but hit a wall. In contrast to all other cases (F, Case, Value and Coalesce), a Subquery cannot be immediately resolved so I'm not too sure how the logic should go.
def get_attribute(obj, attr, default=None):
# ...
elif isinstance(attr, Subquery):
expr = attr.get_source_expressions()[0] # <--- This returns a Query mock
return get_attribute(obj, expr)
parts = attr.split('__') # <--- This is what raises the exception if Subquery is unhandled
Happy to help and contribute if you point me in the right direction.
@stefan6419846 Do you think you could take a look here and help me figure out how to best support annotating using a Subquery?
Certainly not today, but I might have a look at it in a few days. Nevertheless, I have to admit that I have mostly worked on fixing compatibility with more recent Django versions (to allow usage in my own code) and reviewing PRs, while not having a complete overview over the whole module.
That's fair enough. Is there anyone else besides you that might be able to help?
Could you at least tell me what get_attribute is expected to return in each case? Is it the result of the final, resolved expression? e.g.: F("car__model") -> "sedan"? I may be mistaken, but if that's the case, I don't think Subquery would fit in that model.
If in doubt, I would point to @stphivos.
Gotcha. I'll wait for @stphivos to circle back, then.
Thank you!
I'm afraid this one is a bit tricky. Maybe a possible solution would be to somehow use a nested MockSet for Subquery and modify get_attribute to handle that scenario and evaluate it accordingly, but need to explore this approach with different examples. I will try to do so next week but in the meantime if you have any proposals please share them!
I've tried tapping into the sql.Query instance from Subquery, which theoretically, contains all the info to build the query/filters required to be able to resolve the subquery from the model.objects — but that's easier said than done.
In practice, something like this should be feasible:
def get_attribute(obj, attr, default=None):
# ...
if isinstance(attr, Subquery):
subquery_query = attr.query
subquery_model = subquery_query.model
subquery_where = subquery_query.where
for child in subquery_where.children:
if hasattr(child, 'lhs') and hasattr(child, 'rhs'):
field_name = child.lhs.target.name
outer_ref_value = getattr(obj, child.rhs.name)
subquery_result = [item for item in subquery_model.objects.all() if getattr(item, field_name) == outer_ref_value]
return subquery_result[0] if subquery_result else None, None
return None, None
This is not trying to be a comprehensive (or even working) solution. It's only aimed at tackling the query example from my OG comment:
Subquery(ms.filter(value=OuterRef("value"))
The first of many problems is that, currently, passing a MockSet as an argument to Subquery raises a TypeError — which is what #181 tries to address.
A kind of nested MockSet wouldn't be too bad of an idea.
Since Subquery defines its .query as:
self.query = getattr(queryset, "query", queryset).clone()
Having a query property in MockSet like:
@property
def query(self):
return self._mockset_class()(*self.items, clone=self)
Would make the MockSet accessible from a Subquery. Unsure how OuterRef would be handled, though.
@stphivos Any updates here? Would be really nice to have this supported.