Skip to content

Commit 9434dbc

Browse files
Stop reporting a prefetched relation as a batching fallback (v0.19.3).
The batcher writes a path off when the resolver hands back something other than a query, and v0.19.1 started reporting those through the lazy-resolution diagnostic. A prefetched relation hands back the rows it already holds, which takes that same branch, so a perfectly optimized read was reported as falling back to one query per parent. Measured: a depth-two selection under a connection with a scoped leaf runs three statements and now reports nothing, where it previously warned. Genuine fallbacks, where a resolver ran its own query, are unaffected. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9494b65 commit 9434dbc

4 files changed

Lines changed: 48 additions & 10 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "strawberry-orm"
3-
version = "0.19.2"
3+
version = "0.19.3"
44
description = "Unified, backend-agnostic ORM abstraction for Strawberry GraphQL"
55
readme = "README.md"
66
license = "MIT"

src/strawberry_orm/batching.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,36 @@ def stash_parents(execution_context: Any, info: Any, rows: Any) -> None:
9090
store[path_key(info)] = rows
9191

9292

93+
def _already_loaded(root: Any, info: Any) -> bool:
94+
"""True when this field's rows were loaded with the parent.
95+
96+
A prefetched relation hands back the rows it already holds, which is not a
97+
query object and so looks to the rewrite like a resolver that ran its own.
98+
Nothing ran, and nothing is owed a report.
99+
"""
100+
from strawberry_orm.lazy_resolution import (
101+
_django_relation_prefetched,
102+
_sqlalchemy_relation_prefetched,
103+
_tortoise_relation_prefetched,
104+
)
105+
106+
name = getattr(info, "python_name", None) or _path_field_names(info)[-1:]
107+
field_name = name if isinstance(name, str) else (name[0] if name else None)
108+
if field_name is None:
109+
return False
110+
for probe in (
111+
_sqlalchemy_relation_prefetched,
112+
_django_relation_prefetched,
113+
_tortoise_relation_prefetched,
114+
):
115+
try:
116+
if probe(root, field_name) is True:
117+
return True
118+
except Exception: # pragma: no cover - probes are best effort
119+
continue
120+
return False
121+
122+
93123
def record_bail(execution_context: Any, path: str, reason: str) -> None:
94124
"""Note that *path* fell back to one query per parent, and why.
95125
@@ -168,14 +198,15 @@ def resolve(
168198
first = _next(root, info, *args, **kwargs)
169199
if isawaitable(first) or not backend.is_query_object(first):
170200
results[key] = _UNBATCHABLE
171-
record_bail(
172-
execution_context,
173-
key,
174-
"the resolver answers asynchronously, so there is no query to "
175-
"rewrite before it runs"
176-
if isawaitable(first)
177-
else "the resolver ran its own query, leaving nothing to rewrite",
178-
)
201+
if not _already_loaded(root, info):
202+
record_bail(
203+
execution_context,
204+
key,
205+
"the resolver answers asynchronously, so there is no query "
206+
"to rewrite before it runs"
207+
if isawaitable(first)
208+
else "the resolver ran its own query, leaving nothing to rewrite",
209+
)
179210
return first
180211

181212
try:

tests/test_relation_connection_internals.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
_CONNECTION_KEY,
1414
_UNBATCHABLE,
1515
RelationConnectionExtension,
16+
_already_loaded,
1617
_Bail,
1718
extensions_include_relation_connections,
1819
page_attr,
@@ -186,3 +187,9 @@ def test_the_pass_is_installed_only_once():
186187

187188
def test_the_page_is_left_where_the_resolver_looks_for_it():
188189
assert page_attr("posts").endswith("posts")
190+
191+
192+
def test_a_field_with_no_discoverable_name_is_not_treated_as_loaded():
193+
"""Without a name there is nothing to probe, so nothing is assumed."""
194+
nameless = types.SimpleNamespace(path=None, python_name=None)
195+
assert _already_loaded(object(), nameless) is False

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)