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(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:

  1. Framework parses GraphQL query: { users { name posts { title } } }
  2. Creates QueryMeta with field selections and relationships
  3. Injects it into your @query method
  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:
────────────────                 ────────────────────────
{ 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_meta is 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/@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.7.0.tar.gz (138.5 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.7.0-py3-none-any.whl (53.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: sqlmodel_graphql-0.7.0.tar.gz
  • Upload date:
  • Size: 138.5 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

Hashes for sqlmodel_graphql-0.7.0.tar.gz
Algorithm Hash digest
SHA256 5e12c891d04dff8fe4ebd3e643cf6274b3c6ffc5deb18f6e26858652cb873530
MD5 17f04a7aa4d2a43d3980024b7eced673
BLAKE2b-256 2f2a4688f94c48ded9977c6fb9be60474ee1f192baa8fb5b6bbffbab8f3f6e42

See more details on using hashes here.

File details

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

File metadata

  • Download URL: sqlmodel_graphql-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 53.8 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

Hashes for sqlmodel_graphql-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 30353653831387e21a7e3746946eb9ace07af54542719aca3c8b8ece42e789f7
MD5 4e3903556175d0e765f655b705cd9f49
BLAKE2b-256 d685acfa16f57278e32d27b93b5c8e835a920ec2c53e09bdee815cae7f0fea36

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