Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Subquery expressions #182

Open
nfantone opened this issue Sep 4, 2024 · 8 comments
Open

Add support for Subquery expressions #182

nfantone opened this issue Sep 4, 2024 · 8 comments

Comments

@nfantone
Copy link
Contributor

nfantone commented Sep 4, 2024

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
Copy link
Contributor Author

nfantone commented Sep 4, 2024

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

@stefan6419846
Copy link
Collaborator

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.

@nfantone
Copy link
Contributor Author

nfantone commented Sep 4, 2024

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.

@stefan6419846
Copy link
Collaborator

If in doubt, I would point to @stphivos.

@nfantone
Copy link
Contributor Author

nfantone commented Sep 4, 2024

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

Thank you!

@stphivos
Copy link
Owner

stphivos commented Sep 6, 2024

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!

@nfantone
Copy link
Contributor Author

nfantone commented Sep 6, 2024

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
Copy link
Contributor Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants