This release is a pre-release and may not be stable for production use.
sqlalchemy-relay-pagination
Python package that make setting up relay-style pagination with SQLAlchemy easy.
Quickstart
Installation
uv add sqlalchemy-relay-pagination
Schema
Define your connection types in SDL:
type Query {
users(first: Int, last: Int, after: String, before: String): UserConnection!
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
totalCount: Int
}
type User {
id: Int!
name: String!
posts(first: Int, last: Int, after: String, before: String): PostConnection!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
node: Post!
cursor: String!
}
type Post {
id: Int!
title: String!
}
Resolver
from ariadne import QueryType
from sqlalchemy import select
from sqlalchemy_relay_pagination import paginate
from sqlalchemy_relay_pagination.graphql import extract_requested_fields
query_type = QueryType()
@query_type.field("users")
async def resolve_users(_, info, first=None, last=None, after=None, before=None):
session = info.context["session"]
return await paginate(
session,
select(User),
order_by=User.id,
first=first,
last=last,
after=after,
before=before,
requested_fields=extract_requested_fields(info),
)
extract_requested_fields inspects the GraphQL selection set and tells paginate
which fields the client actually requested — skipping the COUNT(*) query when
totalCount is absent, and skipping the extra-row fetch when neither hasNextPage
nor hasPreviousPage is selected.
Batch-loading nested connections (avoiding N+1)
For nested paginated fields (e.g. User.posts), use paginate_many inside a
dataloader to fetch all parents' children in a single query:
from aiodataloader import DataLoader
from ariadne import ObjectType
from sqlalchemy_relay_pagination import paginate_many
user_type = ObjectType("User")
class UserPostsLoader(DataLoader):
def __init__(self, session, first, last, fields):
super().__init__()
self._session = session
self._first = first
self._last = last
self._fields = fields
async def batch_load_fn(self, user_ids):
results = await paginate_many(
self._session,
select(Post),
model=Post,
partition_by=Post.user_id,
order_by=Post.id,
keys=list(user_ids),
first=self._first,
last=self._last,
requested_fields=self._fields,
)
return [results[uid] for uid in user_ids]
@user_type.field("posts")
async def resolve_user_posts(user, info, first=None, last=None):
session = info.context["session"]
loaders = info.context["loaders"]
loader_key = (first, last)
if loader_key not in loaders:
loaders[loader_key] = UserPostsLoader(
session, first, last, extract_requested_fields(info)
)
return await loaders[loader_key].load(user.id)
Note: when
afterorbeforecursors are provided,paginate_manyfalls back to one query per parent key instead of a single batched query, because each parent may be at a different cursor position. Results are still correct; only the single-query optimisation is lost.
Global configuration
Call configure() once at application startup:
from sqlalchemy_relay_pagination import configure, PaginationConfig
configure(PaginationConfig(default_page_size=20, max_page_size=100))
Settings can also be provided via environment variables:
SQLALCHEMY_RELAY_PAGINATION_DEFAULT_PAGE_SIZE=20
SQLALCHEMY_RELAY_PAGINATION_MAX_PAGE_SIZE=100
Resources
Metadata
Release files for sqlalchemy-relay-pagination 0.1.0rc2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sqlalchemy_relay_pagination-0.1.0rc2.tar.gz | 99.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sqlalchemy_relay_pagination-0.1.0rc2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 118.9 kB
Release files / sqlalchemy_relay_pagination-0.1.0rc2.tar.gz
| Download URL | sqlalchemy_relay_pagination-0.1.0rc2.tar.gz |
|---|---|
| Size | 99.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1a68dc51f833c5d3f39be84e89a6dbbb3911ad9bc539d77f869c7044fb77b980
|
|
BLAKE2b-256 checksum How to use checksums |
410cb64fb3558ab0c1b76018a0cfb636a31478898c0205bd86d2768262819f70
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 21, 2026.
Transparency logRelease files / sqlalchemy_relay_pagination-0.1.0rc2-py3-none-any.whl
| Download URL | sqlalchemy_relay_pagination-0.1.0rc2-py3-none-any.whl |
|---|---|
| Size | 19.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3f92677aff68d63b785a97e6d8df0e47c02dd774baf72e921f683774a0af1b79
|
|
BLAKE2b-256 checksum How to use checksums |
0bbc0eafef5ab1a8c49031c9c3598ccad01953d9b3593f1b7ee85e268babda6d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 21, 2026.
Transparency log