Skip to main content

GraphQL SDL generation and query optimization for SQLModel

Project description

SQLModel GraphQL

pypi PyPI Downloads Python Versions

From SQLModel to Running GraphQL API + MCP Server in Minutes

sqlmodel-graphql is the fastest way to build a minimum viable system:

  • Zero Config GraphQL - SQLModel classes → GraphQL schema automatically
  • @query/@mutation Decorators - Mark methods, get endpoints instantly
  • GraphiQL Built-in - Interactive debugging playground
  • One-Line MCP Server - Expose APIs to AI assistants
  • Auto N+1 Prevention - DataLoader batch loading for relationships

No schema files. No resolvers. No boilerplate. Just add decorators to your SQLModel classes.

30-Second Quick Start

from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from pydantic import BaseModel
from sqlmodel import SQLModel, Field, select
from sqlmodel_graphql import query, GraphQLHandler

# 1. Define your model with @query decorator
class User(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str

    @query
    async def get_all(cls) -> list['User']:
        async with get_session() as session:
            return (await session.exec(select(cls))).all()

# 2. Create GraphQL handler (auto-generates schema)
handler = GraphQLHandler(base=SQLModel)

# 3. Setup FastAPI endpoints
class GraphQLRequest(BaseModel):
    query: str

app = FastAPI()

@app.get("/graphql", response_class=HTMLResponse)
async def graphiql():
    return handler.get_graphiql_html()

@app.post("/graphql")
async def graphql(req: GraphQLRequest):
    return await handler.execute(req.query)

Run: uvicorn app:app and visit http://localhost:8000/graphql for the interactive playground.

Features

Rapid Development

  • Zero Config GraphQL - SQLModel classes become GraphQL schema automatically
  • @query/@mutation Decorators - Mark methods as GraphQL operations
  • GraphiQL Integration - Built-in playground for testing and debugging

Smart Optimization

  • Auto N+1 Prevention - DataLoader batch loading for all relationships
  • Level-by-Level Resolution - Relationships loaded in batches per depth level
  • Paginated Relationships - Built-in cursor-free pagination for list relationships

AI-Ready with MCP

  • One-Line MCP Server - config_simple_mcp_server(base=BaseEntity)
  • Progressive Disclosure - AI discovers schema, understands, then queries
  • Multi-App Support - Serve multiple databases through one MCP server

Installation

pip install sqlmodel-graphql

Or with uv:

uv add sqlmodel-graphql
uv add sqlmodel-graphql[fastmcp]  # include mcp server

Demo

running GraphQL demo:

uv run python -m demo.app
# and visit localhost:8000/graphql

running MCP demo

uv run --with fastmcp python -m demo.mcp_server   # stdio mode
uv run --with fastmcp python -m demo.mcp_server --http   # http mode

Quick Start

1. Define Your Models

from typing import Optional
from sqlmodel import SQLModel, Field, Relationship, select
from sqlmodel_graphql import query, mutation

class BaseEntity(SQLModel):
    """Base class for all entities."""
    pass

class User(BaseEntity, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str
    email: str
    posts: list["Post"] = Relationship(
        back_populates="author",
        sa_relationship_kwargs={"order_by": "Post.id"},
    )

    @query
    async def get_all(cls, limit: int = 10) -> list['User']:
        """Get all users."""
        async with get_session() as session:
            stmt = select(cls).limit(limit)
            result = await session.exec(stmt)
            return list(result.all())
    # Generates GraphQL field: userGetAll(limit: Int): [User!]!

    @query
    async def get_by_id(cls, id: int) -> Optional['User']:
        """Get a user by ID."""
        async with get_session() as session:
            stmt = select(cls).where(cls.id == id)
            result = await session.exec(stmt)
            return result.first()
    # Generates GraphQL field: userGetById(id: Int!): User

    @mutation
    async def create(cls, name: str, email: str) -> 'User':
        """Create a new user."""
        async with get_session() as session:
            user = cls(name=name, email=email)
            session.add(user)
            await session.commit()
            await session.refresh(user)
            return user
    # Generates GraphQL field: userCreate(name: String!, email: String!): User!

class Post(BaseEntity, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    title: str
    content: str = ""
    author_id: int = Field(foreign_key="user.id")
    author: Optional[User] = Relationship(back_populates="posts")

Note on relationships: For list relationships, add sa_relationship_kwargs={"order_by": "Entity.column"} to enable pagination support.

2. Create Handler (Auto-generates SDL)

from sqlmodel_graphql import GraphQLHandler

# With pagination support:
handler = GraphQLHandler(
    base=BaseEntity,
    session_factory=async_session,
    enable_pagination=True,
)

# Get the SDL if needed
sdl = handler.get_sdl()
print(sdl)

3. Execute Queries

Try it live: Run uv run uvicorn demo.app:app --reload to start a FastAPI server with GraphiQL at http://localhost:8000/graphql

from sqlmodel_graphql import GraphQLHandler

handler = GraphQLHandler(
    base=BaseEntity,
    session_factory=async_session,
    enable_pagination=True,
)

# Relationships are resolved via DataLoader (no N+1)
result = await handler.execute("""
{
  userGetAll(limit: 5) {
    id
    name
    posts(limit: 3) {
      items {
        title
        author { name }
      }
      pagination { has_more total_count }
    }
  }
}
""")

How Relationship Resolution Works

Relationships are resolved automatically via DataLoader — no manual eager loading needed.

Execution flow:

  1. Root @query method returns entity instances (scalar fields only)
  2. Framework walks the GraphQL selection tree level-by-level
  3. At each level, collects FK values and batch-loads relationships via DataLoader
  4. For paginated lists, uses ROW_NUMBER() window functions for efficient per-parent pagination

Benefits:

  • Automatic N+1 Prevention: All relationships loaded in batched queries
  • Pagination Support: limit/offset on list relationships with has_more and total_count
  • Zero Configuration: No selectinload or load_only needed in user code

Auto-Generated Standard Queries

Pass AutoQueryConfig to GraphQLHandler to automatically generate by_id and by_filter queries for every entity — no @query decorator needed.

Configuration

from sqlmodel_graphql import GraphQLHandler, AutoQueryConfig

config = AutoQueryConfig(
    session_factory=async_session,   # Required: async session factory
    default_limit=10,                # Default limit for by_filter
    generate_by_id=True,             # Generate by_id query
    generate_by_filter=True,         # Generate by_filter query
)

handler = GraphQLHandler(
    base=BaseEntity,
    auto_query_config=config,
    session_factory=async_session,
    enable_pagination=True,
)

When auto_query_config is provided, the handler discovers all entity subclasses (not only those with decorators) and attaches standard queries.

Generated Queries

For an entity named User with primary key id:

Method GraphQL Field Return Type Description
by_id userById(id: Int!): User User | None Find a single entity by primary key
by_filter userByFilter(filter: UserFilterInput, limit: Int): [User!]! list[User] Filter entities by field values

FilterInput is auto-generated per entity. All fields are optional — only non-None values are used as WHERE conditions (exact match).

Example

# Get by ID
{
  userById(id: 1) {
    id
    name
    email
    posts { title }
  }
}

# Filter by fields
{
  userByFilter(filter: { name: "Alice" }, limit: 5) {
    id
    name
    email
  }
}

# List all (no filter)
{
  userByFilter(limit: 20) {
    id
    name
  }
}

Notes:

  • by_id requires exactly one primary key field (detected from id field or primary_key=True).
  • by_filter supports exact match only; for complex queries, write custom @query methods.
  • Existing methods on the entity are not overridden.

MCP Integration

Turn your SQLModel entities into AI-ready tools with a single function call.

Simple MCP Server (Single App)

For single-application scenarios with one database:

from sqlmodel_graphql.mcp import config_simple_mcp_server
from myapp.models import BaseEntity

# Create simplified MCP server - only 3 tools, no app_name required
mcp = config_simple_mcp_server(
    base=BaseEntity,
    name="My Blog API",
    desc="Blog system with users and posts"
)

# Run for AI assistants (Claude Desktop, etc.)
mcp.run()  # stdio mode (default)
# mcp.run(transport="streamable-http")  # HTTP mode

Parameters:

Parameter Type Default Description
base type Required SQLModel base class. All subclasses with @query/@mutation decorators will be automatically discovered.
name str "SQLModel GraphQL API" Name of the MCP server (shown in MCP clients).
desc str | None None Optional description for the GraphQL schema (used for both Query and Mutation type descriptions).
allow_mutation bool False If True, registers graphql_mutation tool and includes Mutation type in schema. Default is read-only mode.

Available Tools (3 tools):

Tool Description
get_schema() Get the complete GraphQL schema in SDL format
graphql_query(query) Execute GraphQL queries
graphql_mutation(mutation) Execute GraphQL mutations

Example: AI Query Flow:

AI: What's available?
    → get_schema() → Returns full SDL

AI: Get users with their posts
    → graphql_query(query="{ userGetUsers(limit: 10) { id name posts { title } } }")

AI: Create a new user
    → graphql_mutation(mutation="mutation { userCreate(name: \"Alice\", email: \"alice@example.com\") { id name } }")

Multi-App MCP Server

For scenarios with multiple independent databases:

from sqlmodel_graphql.mcp import create_mcp_server
from myapp.blog_models import BlogBaseEntity
from myapp.shop_models import ShopBaseEntity

apps = [
    {
        "name": "blog",
        "base": BlogBaseEntity,
        "description": "Blog system API",
    },
    {
        "name": "shop",
        "base": ShopBaseEntity,
        "description": "E-commerce system API",
    }
]

mcp = create_mcp_server(apps=apps, name="My Multi-App API")
mcp.run()

Parameters:

Parameter Type Default Description
apps list[AppConfig] Required List of app configurations. Each app has its own GraphQL schema and independent database.
name str "Multi-App SQLModel GraphQL API" Name of the MCP server (shown in MCP clients).
allow_mutation bool False If True, registers mutation-related tools (list_mutations, get_mutation_schema, graphql_mutation). Default is read-only mode.

AppConfig Fields:

Field Type Required Description
name str Yes Unique identifier for the application (e.g., "blog", "shop").
base type[SQLModel] Yes SQLModel base class for this application.
description str | None No Human-readable description of the application.
query_description str | None No Description for the Query type in GraphQL schema.
mutation_description str | None No Description for the Mutation type in GraphQL schema.

Available Tools (8 tools with app routing):

Tool Description
list_apps() List all available applications
list_queries(app_name) List queries for an app
list_mutations(app_name) List mutations for an app
get_query_schema(name, app_name) Get query schema details
get_mutation_schema(name, app_name) Get mutation schema details
graphql_query(query, app_name) Execute GraphQL queries
graphql_mutation(mutation, app_name) Execute GraphQL mutations

Installation

pip install sqlmodel-graphql[fastmcp]

Running MCP Server

uv run --with fastmcp python -m demo.mcp_server   # stdio mode
uv run --with fastmcp python -m demo.mcp_server --http   # http mode

API Reference

@query

Mark a method as a GraphQL query. The field name is auto-generated as {entityName}{MethodName} in camelCase.

@query
async def get_all(cls, limit: int = 10) -> list['User']:
    """Get all users."""  # Docstring becomes the field description
    ...
# Generates: userGetAll(limit: Int): [User!]!

@mutation

Mark a method as a GraphQL mutation. The field name is auto-generated as {entityName}{MethodName} in camelCase.

@mutation
async def create(cls, name: str, email: str) -> 'User':
    """Create a new user."""
    ...
# Generates: userCreate(name: String!, email: String!): User!

GraphQLHandler

Execute GraphQL queries against SQLModel entities with auto-discovery.

# Recommended: Use base class with session_factory
handler = GraphQLHandler(
    base=BaseEntity,
    session_factory=async_session,
    enable_pagination=True,
)

# Execute queries
result = await handler.execute("{ userGetAll { id name posts { items { title } } } }")

# Get SDL
sdl = handler.get_sdl()

# Get GraphiQL HTML (for interactive playground)
html = handler.get_graphiql_html()  # defaults to /graphql endpoint
html = handler.get_graphiql_html(endpoint="/api/graphql")  # custom endpoint

Parameters:

Parameter Type Default Description
base type Required SQLModel base class for entity discovery.
session_factory Callable | None None Async session factory for DataLoader queries. Required for relationship resolution.
auto_query_config AutoQueryConfig | None None Auto-generate by_id/by_filter queries.
enable_pagination bool False Enable pagination for list relationships.
query_description str | None None Custom description for Query type.
mutation_description str | None None Custom description for Mutation type.

Auto-Discovery Features:

  • Automatically finds all SQLModel subclasses with @query/@mutation decorators
  • Includes all related entities through Relationship fields
  • Supports custom base classes for better organization
  • Recursive discovery of nested relationships

License

MIT License

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sqlmodel_graphql-0.14.0.tar.gz (213.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sqlmodel_graphql-0.14.0-py3-none-any.whl (76.5 kB view details)

Uploaded Python 3

File details

Details for the file sqlmodel_graphql-0.14.0.tar.gz.

File metadata

  • Download URL: sqlmodel_graphql-0.14.0.tar.gz
  • Upload date:
  • Size: 213.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.8.13

File hashes

Hashes for sqlmodel_graphql-0.14.0.tar.gz
Algorithm Hash digest
SHA256 c90d8a59786406e33e02803ce8c37dace4cb857c3b196fc6319b35f1df8bbb63
MD5 eebd74415d1b6e8c0efb8396168072cf
BLAKE2b-256 18f3f7fc24c427890338f73e6fe8738ba9c7b489f643a6237e41d75104c95a79

See more details on using hashes here.

File details

Details for the file sqlmodel_graphql-0.14.0-py3-none-any.whl.

File metadata

File hashes

Hashes for sqlmodel_graphql-0.14.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ea9df2d7653ff465bb537f5f41c2f7d8b7e3b0bb67cbd90ce6e44e92053ecf41
MD5 414e4e709b44b3d0e953cd8ab7ea840c
BLAKE2b-256 30928f9c8c5f7a430a6f9c7fc88843f375b6d07b430863026ae145fe66babe18

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page