strawberry icon indicating copy to clipboard operation
strawberry copied to clipboard

Add Query Batching Support

Open aryaniyaps opened this issue 10 months ago • 8 comments

Add Query Batching Support

Description

This PR adds Query batching support for GraphQL APIs using Strawberry GraphQL.

This makes your GraphQL API compatible with batching features supported by various client side libraries, such as Apollo GraphQL and Relay.

Example (FastAPI):

import strawberry

from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter
from strawberry.schema.config import StrawberryConfig


@strawberry.type
class Query:
    @strawberry.field
    def hello(self) -> str:
        return "Hello World"


schema = strawberry.Schema(
    Query, config=StrawberryConfig(batching_config={"enabled": True, "share_context": True})
)

graphql_app = GraphQLRouter(schema)

app = FastAPI()
app.include_router(graphql_app, prefix="/graphql")

Example (Flask):

import strawberry

from flask import Flask
from strawberry.flask.views import GraphQLView

app = Flask(__name__)


@strawberry.type
class Query:
    @strawberry.field
    def hello(self) -> str:
        return "Hello World"


schema = strawberry.Schema(
    Query, config=StrawberryConfig(batching_config={"enabled": True, "share_context": True})
)

app.add_url_rule(
    "/graphql",
    view_func=GraphQLView.as_view("graphql_view", schema=schema),
)

if __name__ == "__main__":
    app.run()

Note: Query Batching is not supported for multipart subscriptions

Types of Changes

  • [ ] Core
  • [ ] Bugfix
  • [x] New feature
  • [ ] Enhancement/optimization
  • [ ] Documentation

Issues Fixed or Closed by This PR

  • https://github.com/strawberry-graphql/strawberry/issues/1383

Checklist

  • [x] My code follows the code style of this project.
  • [x] My change requires a change to the documentation.
  • [x] I have updated the documentation accordingly.
  • [x] I have read the CONTRIBUTING document.
  • [x] I have added tests to cover my changes.
  • [x] I have tested the changes and verified that they work and don't break anything (as well as I can manage).

Summary by Sourcery

Add support for batching GraphQL queries across all supported frameworks.

New Features:

  • This change introduces support for batching GraphQL queries, enabling clients to send multiple queries in a single request and receive corresponding responses. This is compatible with client-side libraries like Apollo and Relay.

Tests:

  • Added tests to verify query batching functionality and ensure compatibility with existing features.

aryaniyaps avatar Jan 15 '25 06:01 aryaniyaps

Reviewer's Guide by Sourcery

This pull request introduces batch query support for GraphQL APIs, enabling clients to send multiple queries in a single request. This is implemented by adding a "batch" parameter to the view classes and modifying the request handling logic to process lists of queries. Batching is NOT supported for multipart subscriptions.

Sequence diagram for GraphQL batch query processing

sequenceDiagram
    participant Client
    participant GraphQLView
    participant Schema

    Client->>GraphQLView: POST /graphql/batch
    Note over Client,GraphQLView: Request with multiple queries

    GraphQLView->>GraphQLView: parse_http_body()
    GraphQLView->>GraphQLView: validate batch mode

    loop For each query in batch
        GraphQLView->>Schema: execute_single(query)
        Schema-->>GraphQLView: execution result
        GraphQLView->>GraphQLView: process_result()
    end

    GraphQLView-->>Client: Combined response array

Class diagram showing GraphQL view modifications for batch support

classDiagram
    class AsyncBaseHTTPView {
        +bool batch
        +execute_operation()
        +execute_single()
        +parse_http_body()
        +create_response()
    }

    class SyncBaseHTTPView {
        +bool batch
        +execute_operation()
        +execute_single()
        +parse_http_body()
        +create_response()
    }

    class GraphQLRequestData {
        +str query
        +dict variables
        +str operation_name
        +str protocol
    }

    AsyncBaseHTTPView ..> GraphQLRequestData
    SyncBaseHTTPView ..> GraphQLRequestData

    note for AsyncBaseHTTPView "Added batch support"
    note for SyncBaseHTTPView "Added batch support"

Flow diagram for batch query request handling

flowchart TD
    A[Client Request] --> B{Is Batch Request?}
    B -->|Yes| C[Validate Batch Mode]
    B -->|No| D[Process Single Query]

    C --> E{Batch Enabled?}
    E -->|No| F[Return 400 Error]
    E -->|Yes| G[Process Multiple Queries]

    G --> H[Execute Each Query]
    H --> I[Combine Results]
    I --> J[Return Response]

    D --> J

File-Level Changes

Change Details Files
Added batch parameter to view classes
  • A new batch boolean parameter has been added to the view classes across all supported frameworks. This parameter defaults to False.
  • Setting batch=True enables the batch query functionality for the corresponding view.
strawberry/http/async_base_view.py
strawberry/http/sync_base_view.py
strawberry/channels/handlers/http_handler.py
strawberry/flask/views.py
strawberry/asgi/__init__.py
strawberry/fastapi/router.py
strawberry/litestar/controller.py
strawberry/quart/views.py
strawberry/sanic/views.py
strawberry/aiohttp/views.py
strawberry/chalice/views.py
strawberry/django/views.py
tests/http/clients/channels.py
tests/http/clients/django.py
tests/http/clients/aiohttp.py
tests/http/clients/asgi.py
tests/http/clients/async_flask.py
tests/http/clients/chalice.py
tests/http/clients/fastapi.py
tests/http/clients/flask.py
tests/http/clients/litestar.py
tests/http/clients/quart.py
tests/http/clients/sanic.py
tests/http/clients/async_django.py
Modified request handling logic to support batch queries
  • The execute_operation method now checks if the request data is a list. If it is, it treats the request as a batch query and executes each query separately.
  • The parse_http_body method now returns a list of GraphQLRequestData objects for batch queries.
  • The create_response method now handles lists of GraphQLHTTPResponse objects for batch queries.
strawberry/http/async_base_view.py
strawberry/http/sync_base_view.py
strawberry/asgi/__init__.py
strawberry/flask/views.py
strawberry/fastapi/router.py
strawberry/litestar/controller.py
strawberry/quart/views.py
strawberry/sanic/views.py
strawberry/aiohttp/views.py
strawberry/chalice/views.py
strawberry/django/views.py
Added tests for batch query functionality
  • New tests have been added to verify the behavior of batch queries, including successful execution and error handling when batching is disabled or used with multipart subscriptions.
tests/http/test_query_batching.py
Updated client implementations for batch query testing
  • The base HTTP client now accepts a list of JSON objects for the json parameter in the post method to support sending batch queries.
  • The Channels HTTP client has been updated to support batch queries and multipart subscriptions.
tests/http/clients/base.py
tests/http/clients/channels.py
Added a new parameter to HTTPClient implementations for enabling batch queries
  • A batch parameter has been added to the constructors of all HTTP client implementations. This parameter is used to enable or disable batch query support during testing.
tests/http/clients/django.py
tests/http/clients/aiohttp.py
tests/http/clients/asgi.py
tests/http/clients/async_flask.py
tests/http/clients/chalice.py
tests/http/clients/fastapi.py
tests/http/clients/flask.py
tests/http/clients/litestar.py
tests/http/clients/quart.py
tests/http/clients/sanic.py
tests/http/clients/async_django.py

Possibly linked issues

  • #1383: The PR addresses the issue by adding query batching support.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an issue from a review comment by replying to it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull request title to generate a title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in the pull request body to generate a PR summary at any time. You can also use this command to specify where the summary should be inserted.

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

  • Contact our support team for questions or feedback.
  • Visit our documentation for detailed guides and information.
  • Keep in touch with the Sourcery team by following us on X/Twitter, LinkedIn or GitHub.

sourcery-ai[bot] avatar Jan 15 '25 06:01 sourcery-ai[bot]

Thanks for adding the RELEASE.md file!

Here's a preview of the changelog:


Add GraphQL Query batching support

GraphQL query batching is now supported across all frameworks (sync and async) To enable query batching, add a valid batching_config to the schema configuration.

This makes your GraphQL API compatible with batching features supported by various client side libraries, such as Apollo GraphQL and Relay.

Example (FastAPI):

import strawberry

from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter
from strawberry.schema.config import StrawberryConfig


@strawberry.type
class Query:
    @strawberry.field
    def hello(self) -> str:
        return "Hello World"


schema = strawberry.Schema(
    Query, config=StrawberryConfig(batching_config={"max_operations": 10})
)

graphql_app = GraphQLRouter(schema)

app = FastAPI()
app.include_router(graphql_app, prefix="/graphql")

Example (Flask):

import strawberry

from flask import Flask
from strawberry.flask.views import GraphQLView

app = Flask(__name__)


@strawberry.type
class Query:
    @strawberry.field
    def hello(self) -> str:
        return "Hello World"


schema = strawberry.Schema(
    Query, config=StrawberryConfig(batching_config={"max_operations": 10})
)

app.add_url_rule(
    "/graphql/batch",
    view_func=GraphQLView.as_view("graphql_view", schema=schema),
)

if __name__ == "__main__":
    app.run()

Note: Query Batching is not supported for multipart subscriptions

Here's the tweet text:

🆕 Release (next) is out! Thanks to @aryaniyaps for the PR 👏

GraphQL Query Batching support is here! 🍓 ✨ and it works across all frameworks (sync/async)!

Get it here 👉 https://strawberry.rocks/release/(next)

botberry avatar Jan 15 '25 06:01 botberry

Codecov Report

Attention: Patch coverage is 96.46018% with 4 lines in your changes missing coverage. Please review.

Project coverage is 94.41%. Comparing base (741b0bc) to head (fa8ac60). Report is 3 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff            @@
##             main    #3755    +/-   ##
========================================
  Coverage   94.40%   94.41%            
========================================
  Files         527      528     +1     
  Lines       34230    34342   +112     
  Branches     1785     1803    +18     
========================================
+ Hits        32316    32423   +107     
- Misses       1625     1627     +2     
- Partials      289      292     +3     
:rocket: New features to boost your workflow:
  • :snowflake: Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • :package: JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

codecov[bot] avatar Jan 15 '25 06:01 codecov[bot]

CodSpeed Performance Report

Merging #3755 will not alter performance

Comparing aryaniyaps:add-batching (fa8ac60) with main (5510bf1)

Summary

✅ 26 untouched benchmarks

codspeed-hq[bot] avatar Jan 15 '25 06:01 codspeed-hq[bot]

@sourcery-ai review

aryaniyaps avatar Jan 17 '25 04:01 aryaniyaps

hello everyone, since this has been a blocker for quite some time, I've updated the code to raise ValueErrors when context sharing is disabled. Could this PR be merged to add batching support with context sharing?

I'll open a new PR for disabling context sharing!

aryaniyaps avatar Apr 24 '25 01:04 aryaniyaps

@bellini666 let's chat about this at PyCon!

patrick91 avatar May 21 '25 14:05 patrick91

adding notes from the discussion with Patrick today (I might forget this):

  • it might be beneficial to add custom request data to the execution context.
  • right now only the query and variables are set in the context, but adding the request body will be helpful for schema extensions when batching is enabled

Here's an example, in case the above text wasn't clear:

In a persisted queries extension, we get the request body like this:

class PersistedQueriesExtension(SchemaExtension):
    def __init__(self, *, persisted_queries_path: Path) -> None:
        self.cache: dict[str, str] = {}

        with Path.open(persisted_queries_path, "r") as f:
            self.cache = json.load(f)

    async def on_operation(self) -> AsyncIterator[None]:
        body = await self.execution_context.context.get("request").json()
        document_id = body.get("document_id")
        persisted_query = self.cache.get(document_id)

but when batching is enabled, there would be no way to get the right individual request data from the total list of queries passed in.

example:

class PersistedQueriesExtension(SchemaExtension):
    def __init__(self, *, persisted_queries_path: Path) -> None:
        self.cache: dict[str, str] = {}

        with Path.open(persisted_queries_path, "r") as f:
            self.cache = json.load(f)

    async def on_operation(self) -> AsyncIterator[None]:
        body = await self.execution_context.context.get("request").json()
        document_ids = body.get("document_ids")
        # What is the right document ID for this particular query here? there's no way to get it from the list of document IDs...
        # might be beneficial to set something like:
        # custom_request_data = self.execution_context.custom_request_data
        # document_id = custom_request_data.get("document_id")

This should probably be a separate PR

aryaniyaps avatar May 21 '25 15:05 aryaniyaps