Skip to content

Commit e0effd7

Browse files
authored
fix: allow paginating queries that return non scalars (#36)
1 parent d0b3a45 commit e0effd7

3 files changed

Lines changed: 75 additions & 3 deletions

File tree

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ class UserModel(BaseModel):
143143
id: int
144144
name: str
145145

146+
class Config:
147+
orm_mode = True
148+
146149

147150
@router.get("/users", response_model=Page[UserModel])
148151
def all_users(paginate: Paginate = Depends()):
@@ -177,6 +180,47 @@ By default:
177180
}
178181
```
179182

183+
#### Paginating non-scalar results
184+
185+
To paginate a query which doesn't return [scalars], specify `scalars=False` when invoking
186+
`paginate`:
187+
188+
```python
189+
from fastapi import APIRouter, Depends
190+
from fastapi_sqla import Base, Page, Paginate
191+
from pydantic import BaseModel
192+
from sqlalchemy import func, select
193+
from sqlalchemy.orm import relationship
194+
195+
router = APIRouter()
196+
197+
198+
class User(Base):
199+
__tablename__ = "user"
200+
notes = relationship("Note")
201+
202+
203+
class Note(Base):
204+
__tablename__ = "note"
205+
206+
207+
class UserModel(BaseModel):
208+
id: int
209+
name: str
210+
notes_count: int
211+
212+
213+
@router.get("/users", response_model=Page[UserModel])
214+
def all_users(paginate: Paginate = Depends()):
215+
query = (
216+
select(User.id, User.name, func.count(Note.id).label("notes_count"))
217+
.join(Note)
218+
.group_by(User)
219+
)
220+
return paginate(query, scalars=False)
221+
```
222+
223+
180224
#### Customize pagination
181225

182226
You can customize:
@@ -359,3 +403,4 @@ It returns the path of `alembic.ini` configuration file. By default, it returns
359403
[FastAPI background tasks]: https://fastapi.tiangolo.com/tutorial/background-tasks/
360404
[SQLAlchemy]: http://sqlalchemy.org/
361405
[`asyncpg`]: https://magicstack.github.io/asyncpg/current/
406+
[scalars]: (https://docs.sqlalchemy.org/en/14/core/connections.html?highlight=scalars#sqlalchemy.engine.Result.scalars),

fastapi_sqla/__init__.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ def paginate_query(
210210
total_items: int,
211211
offset: int,
212212
limit: int,
213+
scalars: bool = True,
213214
) -> Page[T]: # pragma no cover
214215
"Dispatch on registered functions based on `query` type"
215216
raise NotImplementedError(f"no paginate_query registered for type {type(query)!r}")
@@ -222,6 +223,7 @@ def _paginate_legacy(
222223
total_items: int,
223224
offset: int,
224225
limit: int,
226+
scalars: bool = True,
225227
) -> Page[T]:
226228
total_pages = math.ceil(total_items / limit)
227229
page_number = offset / limit + 1
@@ -243,13 +245,16 @@ def _paginate(
243245
total_items: int,
244246
offset: int,
245247
limit: int,
248+
*,
249+
scalars: bool = True,
246250
) -> Page[T]:
247251
total_pages = math.ceil(total_items / limit)
248252
page_number = offset / limit + 1
249253
query = query.offset(offset).limit(limit)
250254
result = session.execute(query)
255+
data = iter(result.unique().scalars() if scalars else result.unique().mappings())
251256
return Page[T](
252-
data=iter(result.unique().scalars()),
257+
data=data,
253258
meta={
254259
"offset": offset,
255260
"total_items": total_items,
@@ -269,9 +274,11 @@ def dependency(
269274
offset: int = Query(0, ge=0),
270275
limit: int = Query(min_page_size, ge=1, le=max_page_size),
271276
) -> PaginateSignature:
272-
def paginate(query: DbQuery) -> Page[T]:
277+
def paginate(query: DbQuery, scalars=True) -> Page[T]:
273278
total_items = query_count(session, query)
274-
return paginate_query(query, session, total_items, offset, limit)
279+
return paginate_query(
280+
query, session, total_items, offset, limit, scalars=scalars
281+
)
275282

276283
return paginate
277284

tests/test_pagination.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,11 @@ class User(BaseModel):
188188
class Config:
189189
orm_mode = True
190190

191+
class UserWithNotesCount(BaseModel):
192+
id: int
193+
name: str
194+
notes_count: int
195+
191196
@app.get("/v1/users", response_model=Page[User])
192197
def sqla_13_all_users(session: Session = Depends(), paginate: Paginate = Depends()):
193198
query = (
@@ -200,6 +205,18 @@ def sqla_14_all_users(paginate: Paginate = Depends()):
200205
query = select(user_cls).options(joinedload("notes")).order_by(user_cls.id)
201206
return paginate(query)
202207

208+
@app.get("/v2/users-with-notes-count", response_model=Page[UserWithNotesCount])
209+
def sqla_14_all_users_with_notes_count(paginate: Paginate = Depends()):
210+
query = (
211+
select(
212+
user_cls.id, user_cls.name, func.count(note_cls.id).label("notes_count")
213+
)
214+
.join(note_cls)
215+
.order_by(user_cls.id)
216+
.group_by(user_cls)
217+
)
218+
return paginate(query, scalars=False)
219+
203220
def query_count(session: Session, query: Select) -> int:
204221
return session.execute(select(func.count()).select_from(user_cls)).scalar()
205222

@@ -236,6 +253,9 @@ async def client(app):
236253
param(0, 10, "/v2/custom/users", marks=mark.sqlalchemy("1.4")),
237254
param(10, 10, "/v2/custom/users", marks=mark.sqlalchemy("1.4")),
238255
param(40, 2, "/v2/custom/users", marks=mark.sqlalchemy("1.4")),
256+
param(0, 10, "/v2/users-with-notes-count", marks=mark.sqlalchemy("1.4")),
257+
param(10, 10, "/v2/users-with-notes-count", marks=mark.sqlalchemy("1.4")),
258+
param(40, 2, "/v2/users-with-notes-count", marks=mark.sqlalchemy("1.4")),
239259
],
240260
)
241261
async def test_functional(client, offset, items_number, path):

0 commit comments

Comments
 (0)