Skip to main content

GraphQL SDL generation and query optimization for SQLModel

Project description

SQLModel GraphQL

pypi PyPI Downloads Python Versions

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 selectinload and load_only generation
  • 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:

  1. Framework parses GraphQL query: { userGetAll { name posts { title } } }
  2. Creates QueryMeta with field selections and relationships
  3. Injects it into your @query method (if the parameter exists)
  4. 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_meta is 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/@mutation decorators
  • 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


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.10.0.tar.gz (160.3 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.10.0-py3-none-any.whl (61.5 kB view details)

Uploaded Python 3

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

Hashes for sqlmodel_graphql-0.10.0.tar.gz
Algorithm Hash digest
SHA256 dc29a67554466f91721e37e80bd40b4e4cc64c8ac62df050ebad9c873a5a0d96
MD5 f959f117490d85a1c20aa98da955d8c9
BLAKE2b-256 1cf0fd4c37d3f562c56a5dedf48d9a483012dd1645e0823a26657894d0f099ed

See more details on using hashes here.

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

Hashes for sqlmodel_graphql-0.10.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f308311fdaeb0ffda98b22785509e51c238b081aedd907821e8f9ee21beed1d3
MD5 31b44c299192c1027af874806286f7aa
BLAKE2b-256 79fc5c627379ab8782854777340a6e60b48c2d2029d4378452327509f7bd9b78

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