Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 17 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ def get_users(
query: QueryMate = Depends(QueryMate.fastapi_dependency),
db: Session = Depends(get_db)
):
# Returns serialized results (dictionaries)
# Returns serialized results as a list
if query.include_pagination:
return query.run_paginated(db, User)
return query.run(db, User)

@app.get("/users/raw")
Expand Down Expand Up @@ -142,7 +144,9 @@ async def get_users(
query: QueryMate = Depends(QueryMate.fastapi_dependency),
db: AsyncSession = Depends(get_db)
):
# Returns serialized results (dictionaries)
# Returns serialized results
if query.include_pagination:
return await query.run_async_paginated(db, User)
return await query.run_async(db, User)

@app.get("/users/raw")
Expand Down Expand Up @@ -232,18 +236,16 @@ Querymate(sort=[{"posts.visibility": ["private", "internal", "public"]}]).run_ra
### Pagination Metadata Response

In addition to plain lists, you can include pagination metadata alongside items.
Enable it via the query flag or force it via the method parameter:
Use the dedicated paginated methods:

```python
# Force via method
result = query.run(db, User, force_pagination=True)
# Or async:
# result = await query.run_async(db, User, force_pagination=True)
# Sync paginated response
result = query.run_paginated(db, User)

# Respect the query flag / instance setting
result2 = Querymate(include_pagination=True).run(db, User)
# Async paginated response
result = await query.run_async_paginated(db, User)

# Response shape
# Response shape (PaginatedResponse object)
# {
# "items": [{"id": 1, "name": "John"}, ...],
# "pagination": {
Expand All @@ -257,10 +259,12 @@ result2 = Querymate(include_pagination=True).run(db, User)
# }
```

Query flag name and default behavior are configurable (see settings):
The standard `run` and `run_async` methods always return a plain list of items:

- `PAGINATION_PARAM_NAME` (default: `include_pagination`)
- `DEFAULT_RETURN_PAGINATION` (default: `False`)
```python
# Always returns a list[dict[str, Any]]
result = query.run(db, User)
```

### Grouping

Expand Down
50 changes: 25 additions & 25 deletions docs/source/api/querymate.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Convert the QueryMate instance to a query parameter string.
run
~~~

Build and execute the query, returning serialized results.
Build and execute the query, returning a plain list of serialized results.

.. code-block:: python

Expand All @@ -117,29 +117,30 @@ Build and execute the query, returning serialized results.
results = querymate.run(db, User)
# Returns: [{"id": 1, "name": "John", "posts": [{"id": 1, "title": "Post 1"}]}, ...]

Return pagination metadata
~~~~~~~~~~~~~~~~~~~~~~~~~~
run_paginated
~~~~~~~~~~~~~

`run` can optionally return structured pagination metadata along with items. You can enable it via the query payload or force it via method parameter.
Build and execute the query, returning items along with pagination metadata in a typed ``PaginatedResponse`` object.

.. code-block:: python

# Option 1: force via method call
results = querymate.run(db, User, force_pagination=True)
# Option 2: respect the query flag
querymate = Querymate(include_pagination=True)
results = querymate.run(db, User) # will include pagination
# Sync paginated response
result = querymate.run_paginated(db, User)

# Accessing results
print(result.items) # list[dict[str, Any]]
print(result.pagination.total) # int

# Response shape:
# {
# "items": [{"id": 1, "name": "John"}, ...],
# "pagination": {
# "total": 57, # total matching records (ignores limit/offset)
# "page": 2, # current page number (1-based)
# "size": 10, # requested page size (limit)
# "pages": 6, # total pages (ceil(total/size), minimum 1)
# "previous_page": 1, # previous page number or None
# "next_page": 3 # next page number or None
# "total": 57, # total matching records
# "page": 1, # current page number
# "size": 10, # requested page size
# "pages": 6, # total pages
# "previous_page": None,
# "next_page": 2
# }
# }

Expand All @@ -154,29 +155,28 @@ Build and execute the query, returning raw model instances.
# Returns: [<User object>, ...]

run_async
~~~~~~~~
~~~~~~~~~

Build and execute the query asynchronously, returning serialized results.
Build and execute the query asynchronously, returning a plain list of serialized results.

.. code-block:: python

async def get_users():
results = await querymate.run_async(db, User)
# Returns: [{"id": 1, "name": "John"}, ...]

Return pagination metadata (async)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
run_async_paginated
~~~~~~~~~~~~~~~~~~~

The async variant also supports returning pagination data.
Build and execute the query asynchronously, returning items along with pagination metadata in a typed ``PaginatedResponse`` object.

.. code-block:: python

async def get_users():
# Force
result = await querymate.run_async(db, User, force_pagination=True)
# Or respect query flag
result2 = await Querymate(include_pagination=True).run_async(db, User)
# Same shape as the sync variant:
# Async paginated response
result = await querymate.run_async_paginated(db, User)

# Same shape as sync variant:
# {
# "items": [...],
# "pagination": {"total": ..., "page": ..., "size": ..., "pages": ..., "previous_page": ..., "next_page": ...}
Expand Down
19 changes: 10 additions & 9 deletions docs/source/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Next Steps
Pagination Quickstart (Side by Side)
------------------------------------

Enable pagination metadata via the query flag or force it via method parameter.
QueryMate provides dedicated methods for plain lists and paginated responses.

.. list-table::
:header-rows: 1
Expand All @@ -105,24 +105,25 @@ Enable pagination metadata via the query flag or force it via method parameter.

- .. code-block:: python

# Option A: force via method call
result = querymate.run(db, User, force_pagination=True)
# Returns a typed PaginatedResponse
result = querymate.run_paginated(db, User)

# Option B: respect query flag
result2 = Querymate(include_pagination=True).run(db, User)
# Access items and metadata
items = result.items
total = result.pagination.total

# Example
# Example Response
# {
# "items": [
# {"id": 1, "name": "John"},
# {"id": 2, "name": "Jane"}
# ],
# "pagination": {
# "total": 57,
# "page": 2,
# "page": 1,
# "size": 10,
# "pages": 6,
# "previous_page": 1,
# "next_page": 3
# "previous_page": null,
# "next_page": 2
# }
# }
2 changes: 1 addition & 1 deletion docs/source/usage/grouping.rst
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ Response Fields

* ``key``: The group key value (string representation)
* ``items``: Serialized items in this group
* ``pagination``: Pagination metadata for this group
* ``pagination``: Pagination metadata for this group (using the same ``PaginationInfo`` structure as standard paginated queries)

* ``truncated``: ``true`` if ``MAX_LIMIT`` was reached before all groups were filled

Expand Down
54 changes: 37 additions & 17 deletions docs/source/usage/pagination.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,24 +65,28 @@ Response Shape With Metadata
----------------------------

When building UIs, you often need the total number of records and page navigation data.
QueryMate can return a structured response with items and pagination metadata. You can enable it via:
QueryMate provides dedicated methods to return a typed ``PaginatedResponse`` object containing items and pagination metadata.

* Query parameter (respected by default): ``{"include_pagination": true}``
* Instance flag: ``Querymate(include_pagination=True)``
* Method override: ``force_pagination=True/False`` on ``run``/``run_async``
Use the following methods for paginated responses:

* Sync: ``run_paginated(db, model)``
* Async: ``run_async_paginated(db, model)``

.. code-block:: python

# Sync: force pagination regardless of instance flag
result = querymate.run(db, User, force_pagination=True)
# Sync paginated response
result = querymate.run_paginated(db, User)

# Async paginated response
result = await querymate.run_async_paginated(db, User)

# Async: force pagination
result = await querymate.run_async(db, User, force_pagination=True)
# Accessing results
print(len(result.items))
print(result.pagination.total)

# Respect query flag (no force):
result2 = Querymate(include_pagination=True).run(db, User)
The standard ``run`` and ``run_async`` methods always return a plain list of items.

The returned object has the following shape:
The returned ``PaginatedResponse`` object has the following shape:

.. code-block:: json

Expand All @@ -109,9 +113,25 @@ Field semantics:
* ``previous_page``: Previous page number or ``null`` on first page
* ``next_page``: Next page number or ``null`` on last page

Precedence
----------

* ``force_pagination=True``: always include pagination
* ``force_pagination=False``: never include pagination
* ``force_pagination=None`` (default): respect ``include_pagination`` (default is configurable)
Methods Summary
---------------

.. list-table::
:header-rows: 1
:widths: 30 40 30

* - Method
- Return Type
- Description
* - ``run``
- ``list[dict[str, Any]]``
- Plain list of serialized items.
* - ``run_paginated``
- ``PaginatedResponse``
- Items with pagination metadata.
* - ``run_async``
- ``list[dict[str, Any]]``
- Async plain list of items.
* - ``run_async_paginated``
- ``PaginatedResponse``
- Async items with pagination.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ asyncio_mode = "auto"

[tool.ruff.lint.per-file-ignores]
"tests/*" = [ "F811", "B008", "E721",]
"examples/*" = [ "B008",]

[tool.ruff.lint.isort]
known-first-party = [ "querymate",]
12 changes: 4 additions & 8 deletions querymate/core/grouping.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from sqlmodel import SQLModel

from querymate.core.config import settings
from querymate.types import PaginationInfo


class DateGranularity(str, Enum):
Expand Down Expand Up @@ -179,9 +180,7 @@ def is_date_grouping(self) -> bool:
class GroupKeyExtractor:
"""Generates SQL expressions for extracting group keys from columns."""

def __init__(
self, dialect: Literal["postgresql", "sqlite"] = "postgresql"
) -> None:
def __init__(self, dialect: Literal["postgresql", "sqlite"] = "postgresql") -> None:
"""Initialize the extractor with the database dialect.

Args:
Expand Down Expand Up @@ -329,8 +328,8 @@ class GroupResult(BaseModel):
items: list[dict[str, Any]] = Field(
default_factory=list, description="Items in this group"
)
pagination: dict[str, Any] = Field(
default_factory=dict, description="Pagination metadata for this group"
pagination: PaginationInfo = Field(
..., description="Pagination metadata for this group"
)


Expand All @@ -344,6 +343,3 @@ class GroupedResponse(BaseModel):
default=False,
description="True if MAX_LIMIT was reached before all groups were filled",
)



Loading