Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: MrThearMan/graphene-django-query-optimizer
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.2.9
Choose a base ref
...
head repository: MrThearMan/graphene-django-query-optimizer
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v0.2.10
Choose a head ref
  • 2 commits
  • 7 files changed
  • 1 contributor

Commits on Mar 9, 2024

  1. Use field-specific max_limit for prefetch queryset limiting

    MrThearMan committed Mar 9, 2024
    Copy the full SHA
    83d2153 View commit details
  2. Bump version

    MrThearMan committed Mar 9, 2024
    Copy the full SHA
    86b3d85 View commit details
Showing with 74 additions and 6 deletions.
  1. +1 −0 .github/ISSUE_TEMPLATE/bug_report.yml
  2. +1 −1 pyproject.toml
  3. +2 −3 query_optimizer/optimizer.py
  4. +1 −0 query_optimizer/typing.py
  5. +12 −2 query_optimizer/utils.py
  6. +11 −0 tests/conftest.py
  7. +46 −0 tests/test_relay.py
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -35,6 +35,7 @@ body:
If you are not using the latest version, please try to also reproduce the bug
on the latest version before opening the issue.
options:
- "0.2.10"
- "0.2.9"
- "0.2.8"
- "0.2.7"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "graphene-django-query-optimizer"
version = "0.2.9"
version = "0.2.10"
description = "Automatically optimize SQL queries in Graphene-Django schemas."
authors = [
"Matti Lamppu <lamppu.matti.akseli@gmail.com>",
5 changes: 2 additions & 3 deletions query_optimizer/optimizer.py
Original file line number Diff line number Diff line change
@@ -60,7 +60,7 @@ def __init__(self, model: type[Model], info: GQLInfo) -> None:
self.annotations: dict[str, Expression] = {}
self.select_related: dict[str, QueryOptimizer] = {}
self.prefetch_related: dict[str, QueryOptimizer] = {}
self._cache_key: Optional[str] = None # generated during optimization process
self._cache_key: Optional[str] = None # generated during the optimization process
self.total_count: bool = False

def optimize_queryset(
@@ -167,8 +167,7 @@ def paginate_prefetch_queryset(
offset=filter_info.get("filters", {}).get("offset"),
first=filter_info.get("filters", {}).get("first"),
last=filter_info.get("filters", {}).get("last"),
# Just use `RELAY_CONNECTION_MAX_LIMIT` (ignore DjangoConnectionField.max_limit).
max_limit=graphene_settings.RELAY_CONNECTION_MAX_LIMIT,
max_limit=filter_info.get("max_limit", graphene_settings.RELAY_CONNECTION_MAX_LIMIT),
)

# If no pagination arguments are given, and `RELAY_CONNECTION_MAX_LIMIT` is `None`,
1 change: 1 addition & 0 deletions query_optimizer/typing.py
Original file line number Diff line number Diff line change
@@ -123,3 +123,4 @@ class GraphQLFilterInfo(TypedDict, total=False):
filterset_class: Optional[type[FilterSet]]
is_connection: bool
is_node: bool
max_limit: Optional[int]
14 changes: 12 additions & 2 deletions query_optimizer/utils.py
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@
from graphene import Connection
from graphene.relay.node import AbstractNode
from graphene.utils.str_converters import to_snake_case
from graphene_django.settings import graphene_settings
from graphene_django.utils import DJANGO_FILTER_INSTALLED
from graphql import FieldNode, FragmentSpreadNode, GraphQLField, InlineFragmentNode, get_argument_values
from graphql.execution.execute import get_field_def
@@ -301,11 +302,18 @@ def _find_filter_info_from_field_node(

new_parent = get_underlying_type(field_def.type)

# If the field is a relay node field, its `id` field should not be counted as a filter.
is_node = issubclass(getattr(getattr(field_def.resolve, "func", None), "__self__", type(None)), AbstractNode)
is_connection = issubclass(getattr(new_parent, "graphene_type", type(None)), Connection)

# Find the field-specific limit, or use the default limit.
max_limit: Optional[int] = getattr(
getattr(parent.graphene_type, name, None),
"max_limit",
graphene_settings.RELAY_CONNECTION_MAX_LIMIT,
)

# If the field is a connection, we need to go deeper to get the actual field
if is_connection := issubclass(getattr(new_parent, "graphene_type", type(None)), Connection):
if is_connection:
# Find the actual parent object type.
field_def = new_parent.fields["edges"]
new_parent = get_underlying_type(field_def.type)
@@ -327,11 +335,13 @@ def _find_filter_info_from_field_node(

arguments[name] = filter_info = GraphQLFilterInfo(
name=new_parent.name,
# If the field is a relay node field, its `id` field should not be counted as a filter.
filters={} if is_node else filters,
children={},
filterset_class=None,
is_connection=is_connection,
is_node=is_node,
max_limit=max_limit,
)

if DJANGO_FILTER_INSTALLED and hasattr(new_parent, "graphene_type"):
11 changes: 11 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
from graphene_django.utils.testing import graphql_query

from query_optimizer.typing import Callable
from tests.example.types import BuildingNode

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.project.settings")

@@ -45,3 +46,13 @@ def func(*args, **kwargs) -> HttpResponse:
return graphql_query(*args, **kwargs, client=client)

return func


@pytest.fixture()
def _set_building_node_apartments_max_limit() -> int:
limit = BuildingNode.apartments.max_limit
try:
BuildingNode.apartments.max_limit = 1
yield
finally:
BuildingNode.apartments.max_limit = limit
46 changes: 46 additions & 0 deletions tests/test_relay.py
Original file line number Diff line number Diff line change
@@ -144,6 +144,9 @@ def test_optimizer__relay_node__object_type_has_id_filter__nested_filtering(clie
assert edges[0]["node"]["streetAddress"] == apartments[0].street_address


# Connection


def test_optimizer__relay_connection(client_query):
query = """
query {
@@ -509,6 +512,9 @@ def test_optimizer__relay_connection__filtering_empty(client_query):
assert queries == 1, results.log


# Nested


def test_optimizer__relay_connection__nested(client_query):
query = """
query {
@@ -839,3 +845,43 @@ def test_optimizer__relay_connection__nested__filtered_fragment_spread(client_qu
assert queries == 3, results.log
# Check that the filter is actually applied
assert '"example_housingcompany"."name" LIKE' in results.queries[2], results.log


@pytest.mark.usefixtures("_set_building_node_apartments_max_limit")
def test_optimizer__relay_connection__nested__max_limit(client_query):
query = """
query {
pagedBuildings {
edges {
node {
id
apartments {
edges {
node {
id
}
}
}
}
}
}
}
"""

with capture_database_queries() as results:
response = client_query(query)

content = json.loads(response.content)
assert "errors" not in content, content["errors"]

buildings = content["data"]["pagedBuildings"]["edges"]

print(json.dumps(buildings, indent=2))

assert all(len(building["node"]["apartments"]["edges"]) == 1 for building in buildings)

queries = len(results.queries)
# 1 query for counting Buildings
# 1 query for fetching Buildings
# 1 query for fetching nested Apartments
assert queries == 3, results.log