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

Add Query Batching Support #3755

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

aryaniyaps
Copy link
Contributor

@aryaniyaps aryaniyaps commented Jan 15, 2025

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})
)

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})
)

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
  • New feature
  • Enhancement/optimization
  • Documentation

Issues Fixed or Closed by This PR

Checklist

  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • 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.

Copy link
Contributor

sourcery-ai bot commented Jan 15, 2025

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
Loading

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"
Loading

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
Loading

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


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

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey @aryaniyaps - I've reviewed your changes and they look great!

Here's what I looked at during the review
  • 🟡 General issues: 1 issue found
  • 🟡 Security: 1 issue found
  • 🟡 Testing: 2 issues found
  • 🟡 Complexity: 1 issue found
  • 🟢 Documentation: all looks good

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

strawberry/http/async_base_view.py Outdated Show resolved Hide resolved
strawberry/http/async_base_view.py Outdated Show resolved Hide resolved
tests/http/test_query_batching.py Show resolved Hide resolved
tests/http/test_query_batching.py Show resolved Hide resolved
strawberry/http/sync_base_view.py Show resolved Hide resolved
@botberry
Copy link
Member

botberry commented Jan 15, 2025

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, set batching_config.enabled to True in 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={"enabled": 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})
)

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 👏

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

Copy link

codecov bot commented Jan 15, 2025

Codecov Report

Attention: Patch coverage is 95.41985% with 6 lines in your changes missing coverage. Please review.

Project coverage is 97.26%. Comparing base (fa5c2d0) to head (287f615).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3755      +/-   ##
==========================================
- Coverage   97.27%   97.26%   -0.01%     
==========================================
  Files         504      505       +1     
  Lines       33481    33589     +108     
  Branches     5503     5533      +30     
==========================================
+ Hits        32567    32669     +102     
- Misses        703      706       +3     
- Partials      211      214       +3     

Copy link

codspeed-hq bot commented Jan 15, 2025

CodSpeed Performance Report

Merging #3755 will not alter performance

Comparing aryaniyaps:add-batching (a885b67) with main (71f4c11)

Summary

✅ 21 untouched benchmarks

@aryaniyaps aryaniyaps marked this pull request as ready for review January 15, 2025 07:50
request: Request,
request_adapter: AsyncHTTPRequestAdapter,
sub_response: SubResponse,
context: Context,

Choose a reason for hiding this comment

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

Seems like context is shared between all operations?
This may cause unexpected side effects.
Although sometimes that may even be beneficial (for dataloaders etc).

There seems to be no way to isolate request, but it would be nice to have different context/root value per operation in my case.

Copy link
Contributor Author

@aryaniyaps aryaniyaps Jan 15, 2025

Choose a reason for hiding this comment

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

I think that the context should be shared, reusing dataloaders and other resources must be one of the main factors behind adopting query batching, if we want separate context, we can use separate requests with batching disabled, right?

Choose a reason for hiding this comment

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

Well, maybe my use case not that common, because I use context not only for dataloaders, but also for some application logic and some tracing.
Since sharing is simpler - separate context might be a separate feature behind config flag.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, maybe my use case not that common, because I use context not only for dataloaders, but also for some application logic and some tracing. Since sharing is simpler - separate context might be a separate feature behind config flag.

but Im curious @Object905 what benefits would batching bring if the context is not shared?

Choose a reason for hiding this comment

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

Reducing request count mostly to avoid network round-trips.
For example one of my subgraphs is "external" and has ping of about 50ms. But even for "local" sub-graphs this might be noticeable.

Also there are other means to share global data (request itself, thread locals, etc.) besides context.

Copy link
Contributor Author

@aryaniyaps aryaniyaps Jan 16, 2025

Choose a reason for hiding this comment

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

I'm unsure about this, though open-minded, could we get opinions from the maintainers regarding this? What you're suggesting seems like an anti-pattern to me, but it's probably because I haven't used context in that way so far.. @patrick91

Copy link
Member

Choose a reason for hiding this comment

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

After thinking about it, I see advantages on both sides, so I'm slightly leaning towards making context sharing configurable for batching. Here's an example where the entire thing breaks though:

GraphQL Schema:

directive @IN_Context(language: String) on QUERY

type Query {
  product(id: ID!): Product
}

type Product {
  id: ID!
  name: String!
  description: String
}

GraphQL Query Example:

query GetProduct @IN_Context(language: "en") {
  product(id: "1") {
    id
    name
    description
  }
}

Shopify uses this directive to provide localization. A common procedure would involve adding the language inside that directive into context. Now consider two batched queries with different languages and a shared context. One of those languages will be in context, hence, the other response will be broken.

I'm sure we can come up with many other cases where this might cause unexpected behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@erikwrede got it, creating a separate context makes more sense to me now. I'll work on making the context sharing configurable

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've been trying to implement this @erikwrede however I feel like I've hit a roadblock.

I cannot just get a new context like this for every request while using FastAPI:

        if isinstance(request_data, list):
            # batch GraphQL requests
            if not self.schema.config.batching_config["share_context"]:
                tasks = [
                    self.execute_single(
                        request=request,
                        request_adapter=request_adapter,
                        sub_response=sub_response,
                        # create a new context for each request data
                        context=await self.get_context(request, response=sub_response),
                        root_value=root_value,
                        request_data=data,
                    )
                    for data in request_data
                ]

That's because the FastAPI context getter is deeply integrated with the FastAPI DI system, and is run only once per request. what should be done for this use case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

also, I was trying to create a test case, replicating shopify's context driven translation.

I tried to create QUERY level schema directives like this but it wasn't working.

@strawberry.directive(locations=[DirectiveLocation.QUERY], name="in_context")
def in_context(language: str, info: strawberry.Info):
    # put the language in the context here
    print("in_context called")
    info.context.update({"language": language})

am I doing this correctly? I wasn't able to find any documentation/ test cases which were using this type of directive

@aryaniyaps aryaniyaps changed the title Add Query batching support Add Query Batching Support Jan 15, 2025
@aryaniyaps aryaniyaps requested a review from Object905 January 15, 2025 08:18
@aryaniyaps
Copy link
Contributor Author

@sourcery-ai review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants