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
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())
# Generates GraphQL field: userGetAll(limit: Int): [User!]!
@query
async def get_by_id(cls, id: int, query_meta: QueryMeta | None = None) -> Optional['User']:
"""Get a user by ID."""
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()
# Generates GraphQL field: userGetById(id: Int!): User
@mutation
async def create(cls, name: str, email: str, query_meta: QueryMeta | None = None) -> '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
if query_meta:
stmt = select(cls).where(cls.id == user.id)
stmt = stmt.options(*query_meta.to_options(cls))
result = await session.exec(stmt)
return result.first()
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")
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:
{ userGetAll { name posts { title } } } - Creates QueryMeta with field selections and relationships
- Injects it into your
@querymethod (if the parameter exists) 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:
──────────────── ────────────────────────
{ userGetAll { 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
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())
# Generates: userGetUsers: [User!]!
Key Points:
query_metais optional (QueryMeta | None = None) - only injected if the parameter exists- 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 {
"""Get all users with optional query optimization."""
userGetAll(limit: Int): [User!]!
"""Get a user by ID."""
userGetById(id: Int!): User
}
type Mutation {
"""Create a new user. query_meta is injected for relationship loading."""
userCreate(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("""
{
userGetAll(limit: 5) {
id
name
posts {
title
author {
name
}
}
}
}
""")
# Result includes nested relationships automatically:
# {
# "data": {
# "userGetAll": [
# {
# "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.
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
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()
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[mcp]
Running MCP Server
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
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, query_meta: Optional[QueryMeta] = None) -> 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, query_meta: QueryMeta = None) -> '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 if needed
if query_meta:
stmt = select(cls).where(cls.id == user.id)
stmt = stmt.options(*query_meta.to_options(cls))
result = await session.exec(stmt)
return result.first()
return user
# Generates: userCreate(name: String!, email: String!): User!
Note: query_meta is only injected when the method has the parameter in its signature AND 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.10.0.tar.gz.
File metadata
- Download URL: sqlmodel_graphql-0.10.0.tar.gz
- Upload date:
- Size: 160.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","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 |
dc29a67554466f91721e37e80bd40b4e4cc64c8ac62df050ebad9c873a5a0d96
|
|
| MD5 |
f959f117490d85a1c20aa98da955d8c9
|
|
| BLAKE2b-256 |
1cf0fd4c37d3f562c56a5dedf48d9a483012dd1645e0823a26657894d0f099ed
|
File details
Details for the file sqlmodel_graphql-0.10.0-py3-none-any.whl.
File metadata
- Download URL: sqlmodel_graphql-0.10.0-py3-none-any.whl
- Upload date:
- Size: 61.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","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 |
f308311fdaeb0ffda98b22785509e51c238b081aedd907821e8f9ee21beed1d3
|
|
| MD5 |
31b44c299192c1027af874806286f7aa
|
|
| BLAKE2b-256 |
79fc5c627379ab8782854777340a6e60b48c2d2029d4378452327509f7bd9b78
|