GraphQL SDL generation and query optimization for SQLModel
Project description
SQLModel GraphQL
Generate GraphQL APIs & MCP from SQLModel — zero configuration required
No schema files. No resolvers. No boilerplate.
Just decorators and Python. Your SQLModel classes become GraphQL APIs instantly.
Plus: expose your GraphQL via MCP to AI assistants (Claude, GPT, etc.) with three-layer progressive disclosure — AI discovers what's available, understands the schema, then executes queries efficiently.
Features
- Automatic SDL Generation: Generate GraphQL schema from SQLModel classes
- @query/@mutation Decorators: Mark methods as GraphQL operations
- Query Optimization: Parse GraphQL queries to generate optimized SQLAlchemy queries
- N+1 Prevention: Automatic
selectinloadandload_onlygeneration - MCP Integration: Expose GraphQL as MCP tools with progressive disclosure for minimal context usage
Installation
pip install sqlmodel-graphql
Or with uv:
uv add sqlmodel-graphql
uv add sqlmodel-graphql[mcp] # include mcp server
Quick Start
1. Define Your Models
from typing import Optional
from sqlmodel import SQLModel, Field, Relationship, select
from sqlmodel_graphql import query, mutation, QueryMeta
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")
@query(name='users')
async def get_all(cls, limit: int = 10, query_meta: QueryMeta | None = None) -> list['User']:
"""Get all users with optional query optimization."""
async with get_session() as session:
stmt = select(cls).limit(limit)
if query_meta:
# Apply optimization: only load requested fields and relationships
stmt = stmt.options(*query_meta.to_options(cls))
result = await session.exec(stmt)
return list(result.all())
@query(name='user')
async def get_by_id(cls, id: int, query_meta: QueryMeta | None = None) -> Optional['User']:
async with get_session() as session:
stmt = select(cls).where(cls.id == id)
if query_meta:
stmt = stmt.options(*query_meta.to_options(cls))
result = await session.exec(stmt)
return result.first()
@mutation(name='createUser')
async def create(cls, name: str, email: str, query_meta: QueryMeta) -> 'User':
"""Create a new user. query_meta is injected for relationship loading."""
async with get_session() as session:
user = cls(name=name, email=email)
session.add(user)
await session.commit()
await session.refresh(user)
# Re-query with query_meta to load relationships if requested
stmt = select(cls).where(cls.id == user.id)
stmt = stmt.options(*query_meta.to_options(cls))
result = await session.exec(stmt)
return result.first()
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")
Understanding query_meta
The query_meta parameter is automatically injected by the framework to optimize your database queries. It analyzes the GraphQL query's field selections and generates SQLAlchemy optimizations to prevent N+1 queries.
How it works:
- Framework parses GraphQL query:
{ users { name posts { title } } } - Creates QueryMeta with field selections and relationships
- Injects it into your
@querymethod query_meta.to_options(Entity)generates optimized SQLAlchemy options
Benefits:
- Automatic N+1 Prevention: Related data is loaded in batches, not individual queries
- Field Selection: Only requested fields are loaded from database
- Zero Configuration: Works automatically, no manual optimization needed
Example transformation:
GraphQL Query: SQLAlchemy Optimization:
──────────────── ────────────────────────
{ users { select(User).options(
name load_only(User.name),
posts { selectinload(User.posts).options(
title load_only(Post.title)
} )
} )
Without query_meta, loading 10 users with posts would execute:
- 1 query for users
- 10 queries for posts (N+1 problem!)
With query_meta, it executes:
- 1 query for users
- 1 query for all posts (batched!)
Usage Pattern:
@query(name='users')
async def get_users(cls, query_meta: QueryMeta | None = None) -> list['User']:
async with get_session() as session:
stmt = select(cls)
if query_meta:
stmt = stmt.options(*query_meta.to_options(cls))
result = await session.exec(stmt)
return list(result.all())
Key Points:
query_metais optional (QueryMeta | None = None)- Always check
if query_meta:before using - Works with nested relationships of any depth
- For mutations, only injected when returning entity types (not scalars)
2. Create Handler (Auto-generates SDL)
from sqlmodel_graphql import GraphQLHandler
# Create handler - SDL is auto-generated from your models
handler = GraphQLHandler(base=BaseEntity)
# Get the SDL if needed
sdl = handler.get_sdl()
print(sdl)
Output:
type User {
id: Int
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: Int
title: String!
content: String!
author_id: Int!
author: User!
}
type Query {
users(limit: Int): [User!]!
user(id: Int!): User
}
type Mutation {
createUser(name: String!, email: String!): User!
}
3. Execute Queries with GraphQLHandler
from sqlmodel_graphql import GraphQLHandler
# Create handler with base class - auto-discovers all entities
handler = GraphQLHandler(base=BaseEntity)
# Execute a GraphQL query
result = await handler.execute("""
{
users(limit: 5) {
id
name
posts {
title
author {
name
}
}
}
}
""")
# Result includes nested relationships automatically:
# {
# "data": {
# "users": [
# {
# "id": 1,
# "name": "Alice",
# "posts": [
# {"title": "Hello World", "author": {"name": "Alice"}},
# {"title": "GraphQL Tips", "author": {"name": "Alice"}}
# ]
# }
# ]
# }
# }
MCP Integration
Turn your SQLModel entities into AI-ready tools with a single function call.
from sqlmodel_graphql.mcp import create_mcp_server
from myapp.models import BaseEntity
# Create MCP server from your base class
# All SQLModel subclasses with @query/@mutation decorators are auto-discovered
mcp = create_mcp_server(
base=BaseEntity,
name="My Blog API"
)
# Run for AI assistants (Claude Desktop, etc.)
mcp.run() # stdio mode (default)
# mcp.run(transport="streamable-http") # HTTP mode
Available MCP Tools
The server exposes six tools with three-layer progressive disclosure:
Layer 1 - Discover:
- list_queries - List all available queries with names and descriptions
- list_mutations - List all available mutations with names and descriptions
Layer 2 - Understand:
- get_query_schema - Get detailed schema for a specific query (SDL or introspection format)
- get_mutation_schema - Get detailed schema for a specific mutation
Layer 3 - Execute:
- graphql_query - Execute GraphQL queries directly
- graphql_mutation - Execute GraphQL mutations directly
Example: AI Query Flow
AI: What queries are available?
→ list_queries() → Returns: [{"name": "users", "description": "Get all users"}, ...]
AI: Tell me about the "users" query
→ get_query_schema(name="users", response_type="sdl")
→ Returns:
# Query
users(limit: Int): [User!]!
# Related Types
type User {
id: Int!
name: String!
email: String!
posts: [Post!]!
}
AI: Get users with their posts
→ graphql_query(query="{ users(limit: 10) { id name posts { title } } }")
AI: Create a new user
→ graphql_mutation(query="mutation { createUser(name: \"Alice\", email: \"alice@example.com\") { id name } }")
Why MCP?
Three-Layer Progressive Disclosure:
- Layer 1: Lightweight operation lists (~50 tokens)
- Layer 2: On-demand schema details with SDL format
- Layer 3: Direct GraphQL execution
Benefits:
- AI discovers schema dynamically
- SDL format is compact and AI-friendly
- Standard GraphQL syntax - no custom formats to learn
- Minimal context usage for large schemas
Installation
# Core library
pip install sqlmodel-graphql[mcp]
Running MCP Server
# demo/mcp_server.py
uv run python --with mcp demo/mcp_server.py # stdio mode
uv run python --with mcp demo/mcp_server.py --http # HTTP mode
API Reference
@query(name=None, description=None)
Mark a method as a GraphQL query.
@query(name='users', description='Get all users')
async def get_all(cls, limit: int = 10, query_meta: Optional[QueryMeta] = None) -> list['User']:
...
@mutation(name=None, description=None)
Mark a method as a GraphQL mutation. If the mutation returns an entity type, query_meta is automatically injected.
@mutation(name='createUser')
async def create(cls, name: str, email: str, query_meta: QueryMeta) -> '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)
# Re-query with query_meta to load relationships
stmt = select(cls).where(cls.id == user.id)
stmt = stmt.options(*query_meta.to_options(cls))
result = await session.exec(stmt)
return result.first()
Note: query_meta is only injected when the return type is an entity. For scalar returns (e.g., bool, str), it is not passed.
SDLGenerator(entities)
Generate GraphQL SDL from SQLModel classes.
generator = SDLGenerator([User, Post])
sdl = generator.generate()
GraphQLHandler(base)
Execute GraphQL queries against SQLModel entities with auto-discovery.
# Recommended: Use base class for auto-discovery
handler = GraphQLHandler(base=BaseEntity)
# Execute queries
result = await handler.execute("{ users { id name } }")
# Get SDL
sdl = handler.get_sdl()
Auto-Discovery Features:
- Automatically finds all SQLModel subclasses with
@query/@mutationdecorators - Includes all related entities through Relationship fields
- Supports custom base classes for better organization
- Recursive discovery of nested relationships
QueryParser()
Parse GraphQL queries to QueryMeta.
parser = QueryParser()
metas = parser.parse("{ users { id name } }")
# metas['users'] -> QueryMeta(fields=[...], relationships={...})
QueryMeta
Metadata extracted from GraphQL selection set.
@dataclass
class QueryMeta:
fields: list[FieldSelection]
relationships: dict[str, RelationshipSelection]
def to_options(self, entity: type[SQLModel]) -> list[Any]:
"""Convert to SQLAlchemy options for query optimization."""
License
MIT License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sqlmodel_graphql-0.8.0.tar.gz.
File metadata
- Download URL: sqlmodel_graphql-0.8.0.tar.gz
- Upload date:
- Size: 143.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
63dbcbb57eddbf00bbb6e4a90d5d89af7aa17e27bf7b777105159bf8cdf19ee3
|
|
| MD5 |
f41077e1b1d1bf49758e8a109305d99f
|
|
| BLAKE2b-256 |
25741582dad4bfa86509bbe6f57266cbe126db2819523fe67b81d032663496b0
|
File details
Details for the file sqlmodel_graphql-0.8.0-py3-none-any.whl.
File metadata
- Download URL: sqlmodel_graphql-0.8.0-py3-none-any.whl
- Upload date:
- Size: 58.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dbe17d5177fa9c36a28fe011ae0989638124558697f1e9e7c83528187ab49f3c
|
|
| MD5 |
bde59fa802b72338b64e4f2ebeda35a6
|
|
| BLAKE2b-256 |
08f06cdc2f49e7e2a7a049445b63f4883444ac0b78020be50135cb654366a266
|