django-mock-queries icon indicating copy to clipboard operation
django-mock-queries copied to clipboard

Add support for Subquery expressions

Open nfantone opened this issue 1 year ago • 8 comments

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.

nfantone avatar Sep 04 '24 12:09 nfantone

@stefan6419846 Do you think you could take a look here and help me figure out how to best support annotating using a Subquery?

nfantone avatar Sep 04 '24 15:09 nfantone

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.

stefan6419846 avatar Sep 04 '24 15:09 stefan6419846

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.

nfantone avatar Sep 04 '24 15:09 nfantone

If in doubt, I would point to @stphivos.

stefan6419846 avatar Sep 04 '24 15:09 stefan6419846

Gotcha. I'll wait for @stphivos to circle back, then.

Thank you!

nfantone avatar Sep 04 '24 15:09 nfantone

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!

stphivos avatar Sep 06 '24 14:09 stphivos

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.

nfantone avatar Sep 06 '24 16:09 nfantone

@stphivos Any updates here? Would be really nice to have this supported.

nfantone avatar Sep 11 '24 12:09 nfantone