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

Fixed #25534, Fixed #31639 -- Added support for transform references in expressions. #13685

Merged
merged 1 commit into from Nov 27, 2020

Conversation

LilyFoote
Copy link
Contributor

@LilyFoote LilyFoote commented Nov 15, 2020

By updating Query.resolve_ref to return the transform instead of the field, we enable F objects to include transforms.

This resolves ticket-25534 and ticket-31639.

Copy link
Member

@charettes charettes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a feeling this PR does more than solve ticket-25534 for the aggregation case.

This was a larger problem regarding all transform references as detailed in ticket-31639 which makes be believe this PR should be renamed Added support for transform references in expressions..

@Ian-Foote did you test this out on anything but the simple aggregation case? I think we should also test F and OuterRef support for transform resolution as detailed in ticket-31639 because these also go through resolve_ref and setup_joins.

tests/aggregation/tests.py Show resolved Hide resolved
@LilyFoote
Copy link
Contributor Author

You're right, I think this does address ticket-31639 too. I actually saw that ticket first via twitter, which inspired me to see if this could be fixed instead of just raising an error as that ticket suggests, especially since I've had this itch myself before too.

@LilyFoote LilyFoote changed the title Fixed #25534 -- Support lookups in aggregation Fixed #25534 -- Added support for transform references in expressions. Nov 18, 2020
docs/releases/3.2.txt Outdated Show resolved Hide resolved
docs/topics/db/queries.txt Outdated Show resolved Hide resolved
@felixxm felixxm self-assigned this Nov 19, 2020
Copy link
Member

@felixxm felixxm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ian-Foote Thanks 👍 This is brilliant 💎 I left some comments.

We should update docs and add versionchanged annotations to:

Can we add tests for index and slice transforms for ArrayField() and for key transforms for HStoreField()?

django/db/models/sql/query.py Show resolved Hide resolved
tests/annotations/tests.py Show resolved Hide resolved
docs/releases/3.2.txt Outdated Show resolved Hide resolved
docs/topics/db/queries.txt Outdated Show resolved Hide resolved
tests/annotations/tests.py Outdated Show resolved Hide resolved
docs/topics/db/queries.txt Outdated Show resolved Hide resolved
docs/topics/db/queries.txt Outdated Show resolved Hide resolved
docs/topics/db/queries.txt Outdated Show resolved Hide resolved
@felixxm
Copy link
Member

felixxm commented Nov 19, 2020

Can we also add tests for other expressions and for chaining transforms?, e.g.

    def test_chaining_transforms(self):
        Author.objects.create(name=' John  ')
        Author.objects.create(name='Rhonda')
        with register_lookup(CharField, Trim), register_lookup(CharField, Length):
            for expr in [Length('name__trim'), F('name__trim__length')]:
                self.assertCountEqual(
                    Author.objects.annotate(length=expr).values('name', 'length'),
                    [{'name': ' John  ', 'length': 4}, {'name': 'Rhonda', 'length': 6}],
                )

@felixxm felixxm changed the title Fixed #25534 -- Added support for transform references in expressions. Fixed #25534, Fixed #31639 -- Added support for transform references in expressions. Nov 19, 2020
@LilyFoote LilyFoote force-pushed the ticket_25534 branch 5 times, most recently from 9b0c504 to 36303d3 Compare November 21, 2020 21:31
Comment on lines 693 to 700
>>> Entry.objects.values('pub_date__year').annotate(
top_rating=Subquery(
Entry.objects.filter(
pub_date__year=OuterRef('pub_date__year')
).order_by('-rating').values('rating')[:1]
),
total_comments=Sum('number_of_comments'),
)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't entirely like this example - it's a bit more complicated than would be ideal.

I considered doing just:

    >>> Entry.objects.annotate(
            top_rating=Subquery(
                Entry.objects.filter(
                    pub_date__year=OuterRef('pub_date__year')
                ).order_by('-rating').values('rating')[:1]
            ),
        )

But I don't like that this leads to duplicate rows in the resulting queryset - we get one for each Entry sharing pub_date__year. The example with Sum consolidates these into just one row.

I'm not sure explaining these nuances is in scope here - I'd prefer an example that completely avoids them.

@LilyFoote
Copy link
Contributor Author

Thinking about this a bit more - I think all my tests for this have been using transforms and I'm not sure the idea makes sense for lookups. Can someone think of an example where a lookup would be used, or should I drop mentions of lookups from the documentation?

@felixxm
Copy link
Member

felixxm commented Nov 22, 2020

Thinking about this a bit more - I think all my tests for this have been using transforms and I'm not sure the idea makes sense for lookups. Can someone think of an example where a lookup would be used, or should I drop mentions of lookups from the documentation?

True, good catch 🎯, let's chop "lookups". transform_function() should even raise a FieldError for a lookup, see

# Verify that the last lookup in name is a field or a transform:
# transform_function() raises FieldError if not.
join_info.transform_function(targets[0], final_alias)

@felixxm
Copy link
Member

felixxm commented Nov 23, 2020

@Ian-Foote Thanks for updates 👍 I pushed small edits to tests. I'm going to check docs later today.

@LilyFoote
Copy link
Contributor Author

I've tweaked the first commit message to stop mentioning lookups and squashed the commits while I was at it.

@felixxm
Copy link
Member

felixxm commented Nov 23, 2020

@Ian-Foote Thanks 👍 I added some tests for key transforms for JSONField() and one of them doesn't work, can you take a look? model_fields.test_jsonfield.TestQuerying.test_key_transform_annotation_expression.

@LilyFoote
Copy link
Contributor Author

@felixxm That was a tricky one! It turns out that filtering with an F object with a transform based from an annotated field didn't work. The change I've pushed resolves the problem in this case, but there may be other scenarios we need to test.

I also tweaked the test so the filter actually served a purpose.

@felixxm
Copy link
Member

felixxm commented Nov 26, 2020

@Ian-Foote Thanks for updates 👍 I found one more case that doesn't work:

        self.assertSequenceEqual(
            RelatedJSONModel.objects.annotate(
                key=F('value__d'),
                related=F('json_model'),
                chain=F('key__1'),
                expr=Cast('key', models.JSONField()),
            ).filter(chain=F('related__value__d__0')),
            [related_obj],
        )

it raises "django.core.exceptions.FieldError: Unsupported lookup 'value' for AutoField or join on the field not permitted.". It's probably unrelated because even without this patch a simple related annotation fails, e.g.:

RelatedJSONModel.objects.annotate(
    related=F('json_model'),
).annotate(chain=F('related__value')

raises "django.core.exceptions.FieldError: Cannot resolve keyword 'value' into field. Join on 'related' not permitted."

@LilyFoote
Copy link
Contributor Author

I agree - I think fixing this one is a fair bit of work and I'd prefer to handle it separately.

@felixxm
Copy link
Member

felixxm commented Nov 27, 2020

I agree - I think fixing this one is a fair bit of work and I'd prefer to handle it separately.

My mistake, this behavior is well documented: "When referencing relational fields such as ForeignKey, F() returns the primary key value rather than a model instance:" ☺️

I'm going to work on final edits.

@LilyFoote
Copy link
Contributor Author

I think it's something that could be made to work, but I'm definitely happy to leave it out of this ticket/PR.

@felixxm felixxm force-pushed the ticket_25534 branch 2 times, most recently from 5c14183 to 5cea9f9 Compare November 27, 2020 10:56
@felixxm
Copy link
Member

felixxm commented Nov 27, 2020

@Ian-Foote I pushed edits to docs, and added tests for windows expressions, QuerySet.alias(), QuerySet.update(), F().bitor(), etc.

Unfortunately QuerySet.update() raises django.core.exceptions.FieldError: Joined field references are not permitted in this query, IMO that's the last 🐛 🤞

@felixxm
Copy link
Member

felixxm commented Nov 27, 2020

I will try to remove:

if not allow_joins and LOOKUP_SEP in name:
raise FieldError("Joined field references are not permitted in this query")

it looks redundant and unnecessary (\cc @charettes).

@charettes
Copy link
Member

charettes commented Nov 27, 2020

Unfortunately QuerySet.update() raises django.core.exceptions.FieldError: Joined field references are not permitted in this query, IMO that's the last 🐛 🤞

Does this happen when resolving transforms as well? I know there's a check in place to prevent JOIN references in UPDATE because that's requires a subquery pushdown (ticket-25643, ticket-14104) but I guess this would need to be adjusted.

it looks redundant and unnecessary (\cc @charettes).

Yeah it looks redundant to me as well since we already check it when trying to resolve a non-annotation. Was it addressing the update issue?

@felixxm
Copy link
Member

felixxm commented Nov 27, 2020

Unfortunately QuerySet.update() raises django.core.exceptions.FieldError: Joined field references are not permitted in this query, IMO that's the last 🐛 🤞

Does this happen when resolving transforms as well? I know there's a check in place to prevent JOIN references in UPDATE because that's requires a subquery pushdown (ticket-25643, ticket-14104) but I guess this would need to be adjusted.

it looks redundant and unnecessary (\cc @charettes).

Yeah it looks redundant to me as well since we already check it when trying to resolve a non-annotation. Was it addressing the update issue?

Yes, without causing any failures.

…in expressions.

Thanks Mariusz Felisiak and Simon Charette for reviews.
@felixxm felixxm merged commit 8b040e3 into django:master Nov 27, 2020
@LilyFoote LilyFoote deleted the ticket_25534 branch November 27, 2020 23:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants