-
-
Notifications
You must be signed in to change notification settings - Fork 157
feat: support aliasing fields with optimizer hint callables #930
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
base: main
Are you sure you want to change the base?
Changes from all commits
3c6fdfd
5b0b59a
f4c03c6
c7d2908
60c2c8a
6f660ca
98cd77b
1b68df0
f82de03
f302415
864aa7d
6cf9b61
57bca84
b60c589
c20a244
970f0da
2f400d7
eade6b3
2369023
529116b
4e3d9dc
09fe07f
b7ed3e8
f666486
17bb07d
a79a32d
9b4af3c
33afe5f
3c894cf
e1ff781
6b11808
bbb3b23
61bd040
ef694e0
fabc5eb
3baf600
a9aed79
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| --- | ||
| release type: minor | ||
| --- | ||
|
|
||
| Adjust the optimizer to handle when the same field is selected multiple times via aliases with different arguments and the field doesn't have a custom resolver. | ||
|
|
||
| Also add support for aliasing custom fields with `annotate`/`prefetch_related` optimizer hint callables, together with three new public helpers (see the [optimizer guide](https://strawberry-graphql.github.io/strawberry-django/guide/optimizer/) for examples): | ||
|
|
||
| - `optimizer_hint_key(info)`: a unique, deterministic attribute name for the current field selection (based on the alias), stable between the hint callable and the resolver. | ||
| - `get_hint_value(source, info, default_attr=None, *, default=...)`: reads the value produced by an optimizer hint for the current selection. | ||
| - `get_field_arguments(info)`: resolves the coerced argument values (including variables and schema defaults) of the current field selection, for use inside hint callables. | ||
|
|
||
| Note: inside an optimizer hint callable (and any `get_queryset` reached through the optimizer's prefetch path), `info.path.key` now reports the selection's response key — the alias when the field is aliased — rather than always the field name. This is what makes the per-alias hints deterministic, but code that keys cache or permission logic on `info.path` inside a hint callable should account for it. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -287,6 +287,122 @@ The following options are accepted for optimizer hints: | |
| or a callable in the format of `Callable[[Info], BaseExpression]` | ||
| (e.g. `annotate={"total": lambda info: Sum(...)}`) | ||
|
|
||
| ## Aliased fields with optimization hint callables | ||
|
|
||
| A field whose hints are callables can produce argument-dependent optimizations, | ||
| which means the same field can be selected multiple times via | ||
| [aliases](https://graphql.org/learn/queries/#aliases) with different arguments: | ||
|
|
||
| ```graphql | ||
| query { | ||
| orderItems { | ||
| cheap: totalFiltered(maxPrice: 10) | ||
| expensive: totalFiltered(maxPrice: 1000) | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| Each alias resolves the hint callables with its own `info`, scoped to that | ||
|
rcybulski1122012 marked this conversation as resolved.
|
||
| specific selection. Since every annotation and `Prefetch(to_attr=...)` needs a | ||
| unique name, three helpers keep the hint and the resolver in sync: | ||
|
|
||
| - `strawberry_django.optimizer_hint_key(info)` names the annotation or | ||
| `Prefetch(to_attr=...)` inside the hint callable. | ||
| - `strawberry_django.get_hint_value(...)` reads that value back inside the | ||
| resolver. | ||
| - `strawberry_django.get_field_arguments(info)` resolves the current | ||
| selection's coerced arguments inside the hint callable. | ||
|
|
||
| See each helper's docstring for the exact arguments and lookup order. | ||
|
|
||
| For annotations, callable values are automatically stored under the | ||
| alias-scoped name when the field is aliased, so the resolver only needs | ||
| `get_hint_value`: | ||
|
|
||
| ```python title="types.py" | ||
| @strawberry_django.type(models.Order) | ||
| class Order: | ||
| @strawberry_django.field( | ||
| annotate=lambda info: Sum( | ||
| "items__price", | ||
| filter=Q(items__price__lte=get_field_arguments(info)["maxPrice"]), | ||
| ), | ||
| ) | ||
| def total_filtered(self, root: models.Order, info: Info, max_price: int) -> int: | ||
| return get_hint_value(root, info, "total_filtered", default=0) | ||
| ``` | ||
|
|
||
| For prefetches, name the `to_attr` with `optimizer_hint_key` and read it back | ||
| the same way: | ||
|
|
||
| ```python title="types.py" | ||
| @strawberry_django.type(models.Order) | ||
| class Order: | ||
| @strawberry_django.field( | ||
| prefetch_related=lambda info: Prefetch( | ||
| "items", | ||
| queryset=models.OrderItem.objects.filter( | ||
| price__lte=get_field_arguments(info)["maxPrice"], | ||
| ), | ||
| to_attr=optimizer_hint_key(info), | ||
| ), | ||
| ) | ||
| def items_filtered( | ||
| self, root: models.Order, info: Info, max_price: int | ||
| ) -> list[OrderItem]: | ||
| value = get_hint_value(root, info, default=None) | ||
| if value is None: | ||
| # The optimizer is turned off, resolve the value directly | ||
| value = list(root.items.filter(price__lte=max_price)) | ||
| return value | ||
| ``` | ||
|
|
||
| A field can also annotate multiple values and combine them in the resolver. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Claude raised this concern to me: This third example reteaches what the two above it already showed, with a different aggregate. The annotate example, the prefetch example and the static-annotation note carry the section on their own.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure about it. I think a more complex example with usage of the |
||
| Declare the annotations as a dict with custom labels and read each one back | ||
| with `get_hint_value` passing the label as `default_attr` - when the field is | ||
| aliased, callable annotations are stored under an alias-scoped variant of the | ||
| label, and `get_hint_value` checks that variant first: | ||
|
|
||
| ```python title="types.py" | ||
| @strawberry_django.type(models.Order) | ||
| class Order: | ||
| @strawberry_django.field( | ||
| annotate={ | ||
| "_matching_count": lambda info: Count( | ||
| "items", | ||
| filter=Q(items__price__lte=get_field_arguments(info)["maxPrice"]), | ||
| ), | ||
| "_matching_total": lambda info: Sum( | ||
| "items__price", | ||
| filter=Q(items__price__lte=get_field_arguments(info)["maxPrice"]), | ||
| ), | ||
| }, | ||
| ) | ||
| def items_summary(self, root: models.Order, info: Info, max_price: int) -> str: | ||
| count = get_hint_value(root, info, "_matching_count") | ||
| total = get_hint_value(root, info, "_matching_total") | ||
| return f"{count} items, {total} total" | ||
| ``` | ||
|
|
||
| > [!NOTE] | ||
| > Static (non-callable) annotate expressions can't depend on the field's | ||
| > arguments, so they keep their shared label and are annotated only once, no | ||
| > matter how many aliases select the field. | ||
|
|
||
| > [!NOTE] | ||
| > Aliases of a callable-hint field are collapsed into a single annotation / | ||
| > `Prefetch` when they are called with identical arguments **and** resolve to | ||
| > the same target - a shared annotation label, or a `Prefetch` with a fixed | ||
| > `to_attr` (or a plain relation string). Aliases with different arguments, or | ||
| > whose `to_attr` is derived from `optimizer_hint_key(info)` (which is unique | ||
| > per alias), each get their own annotation / prefetch. | ||
| > | ||
| > In particular, a `prefetch_related` that names its `to_attr` with | ||
| > `optimizer_hint_key` targets a distinct attribute per alias, so every such | ||
| > alias costs one additional query - even when the arguments are identical. | ||
| > If you want identical-argument aliases to share a single prefetch, give the | ||
| > `Prefetch` a fixed `to_attr` instead. | ||
|
|
||
| ## Optimization hints on model (ModelProperty) | ||
|
|
||
| It is also possible to include type hints directly in the models' `@property` | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.