Declarative Repositories & Queries¶
Pico-SQLAlchemy provides a powerful repository pattern that combines Implicit Transactions with Declarative Query Execution.
This allows you to write data access layers with minimal boilerplate, focusing only on the query logic or the business operation.
1. The @repository Decorator¶
The @repository decorator marks a class as a Pico-IoC component (singleton by default).
Automatic Transaction Management¶
Crucially, any public async method defined in a @repository class is automatically wrapped in a Read-Write Transaction (propagation="REQUIRED").
This means you do not need to manually add @transactional to your CRUD methods.
from pico_sqlalchemy import repository, SessionManager, get_session
@repository
class UserRepository:
def __init__(self, manager: SessionManager):
self.manager = manager
# Implicitly transactional (Read-Write)
async def save(self, user: User) -> User:
session = get_session(self.manager)
session.add(user)
return user
Entity Binding¶
You can optionally bind a repository to a specific SQLAlchemy model using the entity parameter. This is required if you want to use Expression Mode in @query.
2. Declarative Queries (@query)¶
The @query decorator allows you to define database queries declaratively.
- No Implementation Needed: The method body is ignored. The library intercepts the call, binds arguments, executes the query, and maps the result.
- Implicit Read-Only Transaction: All
@querymethods run automatically in a Read-Only transaction for performance safety.
Mode A: Expression Mode (expr)¶
This is the most concise mode. It requires @repository(entity=Model). You only provide the WHERE clause.
Features:
- Automatic
SELECT * FROM tablegeneration. - Supports Dynamic Sorting via
PageRequest.
@repository(entity=User)
class UserRepository:
# Effectively runs: SELECT * FROM users WHERE username = :username
@query(expr="username = :username", unique=True)
async def find_by_username(self, username: str) -> User | None:
...
# Effectively runs: SELECT * FROM users WHERE active = true
@query(expr="active = true")
async def find_all_active(self) -> list[User]:
...
Mode B: SQL Mode (sql)¶
Use this for full control, joins, raw SQL, or complex projections. It does not require an entity binding on the repository.
Features:
- Full control over the SQL string.
- Does NOT support Dynamic Sorting via
PageRequest. You must write theORDER BYclause yourself.
Usage:
You can use the standard @query(sql="...") syntax or the @query.sql("...") shortcut.
@repository
class StatsRepository:
# Option 1: Standard syntax
@query(sql="SELECT count(*) as total FROM users")
async def count_users(self) -> int:
...
# Option 2: Shortcut syntax (Cleaner)
@query.sql(
"SELECT u.name, p.title FROM users u "
"JOIN posts p ON u.id = p.user_id "
"WHERE u.id = :uid "
"ORDER BY u.name"
)
async def get_user_posts(self, uid: int) -> list[dict]:
...
3. Pagination & Sorting¶
Pico-SQLAlchemy provides built-in support for offset-based pagination and dynamic sorting.
Basic Pagination¶
- Set
paged=Truein the@querydecorator. - Add a parameter explicitly named
pageof typePageRequestto your method signature. - Set the return type to
Page[T].
The library automatically generates the count query and applies limits/offsets.
Important: The argument must be named
page. Using other names (e.g.,req,pagination) will result in a runtime error or ignored pagination.
from pico_sqlalchemy import Page, PageRequest
@repository(entity=User)
class UserRepository:
@query(expr="active = true", paged=True)
async def find_active_paged(self, page: PageRequest) -> Page[User]:
...
# Usage: Get page 0, size 10
# await repo.find_active_paged(PageRequest(page=0, size=10))
Dynamic Sorting¶
You can request dynamic sorting by passing a list of Sort objects within the PageRequest.
⚠️ Warning: Expression Mode Only
Dynamic sorting (injecting
ORDER BYclauses based onPageRequest) works only in Expression Mode (expr).If you use SQL Mode (
sql) and provide aPageRequestwith sorts, the library will raise aValueError. This is a security measure to prevent ambiguity and injection risks in raw SQL.
Security Note: To prevent SQL Injection, dynamic sorting in expr mode validates that the requested sort fields exist in the underlying SQLAlchemy model columns. If a field is invalid, a ValueError is raised.
from pico_sqlalchemy import PageRequest, Sort
# Usage: Sort by name ASC, then age DESC
request = PageRequest(
page=0,
size=20,
sorts=[
Sort(field="name", direction="ASC"),
Sort(field="age", direction="DESC")
]
)
# Works automatically because this method uses @query(expr=...)
await repo.find_active_paged(request)
| Parameter | Description |
|---|---|
field | The name of the column in the model (e.g., "username"). |
direction | "ASC" (default) or "DESC". |
4. Return Types & Mapping¶
The @query interceptor is smart about return types:
unique=True: Returns a single scalar/object orNone(usesresult.scalars().first()or mappings).- Default (List): Returns a list of scalars/objects (uses
result.scalars().all()or mappings). - Raw SQL: If the query returns multiple columns (not a mapped entity), it returns dictionary-like mappings.
5. Summary of Behavior¶
| Decorator | Transaction Mode | Execution Logic |
|---|---|---|
@repository (Plain Method) | Read-Write | Executes your Python code body. |
@query | Read-Only | Ignores body; executes SQL/Expr automatically. |
@transactional | Explicit | Executes your Python code body with custom config. |
```# Declarative Repositories & Queries
Pico-SQLAlchemy provides a powerful repository pattern that combines **Implicit Transactions** with **Declarative Query Execution**.
This allows you to write data access layers with minimal boilerplate, focusing only on the query logic or the business operation.
---
## 1. The `@repository` Decorator
The `@repository` decorator marks a class as a Pico-IoC component (singleton by default).
### Automatic Transaction Management
Crucially, **any public `async` method** defined in a `@repository` class is automatically wrapped in a **Read-Write Transaction** (`propagation="REQUIRED"`).
This means you do **not** need to manually add `@transactional` to your CRUD methods.
```python
from pico_sqlalchemy import repository, SessionManager, get_session
@repository
class UserRepository:
def __init__(self, manager: SessionManager):
self.manager = manager
# Implicitly transactional (Read-Write)
async def save(self, user: User) -> User:
session = get_session(self.manager)
session.add(user)
return user
````
### Entity Binding
You can optionally bind a repository to a specific SQLAlchemy model using the `entity` parameter. This is required if you want to use **Expression Mode** in `@query`.
```python
@repository(entity=User)
class UserRepository:
...
2. Declarative Queries (@query)¶
The @query decorator allows you to define database queries declaratively.
- No Implementation Needed: The method body is ignored. The library intercepts the call, binds arguments, executes the query, and maps the result.
- Implicit Read-Only Transaction: All
@querymethods run automatically in a Read-Only transaction for performance safety.
Mode A: Expression Mode (expr)¶
This is the most concise mode. It requires @repository(entity=Model). You only provide the WHERE clause.
Features:
- Automatic
SELECT * FROM tablegeneration. - Supports Dynamic Sorting via
PageRequest.
@repository(entity=User)
class UserRepository:
# Effectively runs: SELECT * FROM users WHERE username = :username
@query(expr="username = :username", unique=True)
async def find_by_username(self, username: str) -> User | None:
...
# Effectively runs: SELECT * FROM users WHERE active = true
@query(expr="active = true")
async def find_all_active(self) -> list[User]:
...
Mode B: SQL Mode (sql)¶
Use this for full control, joins, raw SQL, or complex projections. It does not require an entity binding on the repository.
Features:
- Full control over the SQL string.
- Does NOT support Dynamic Sorting via
PageRequest. You must write theORDER BYclause yourself.
@repository
class StatsRepository:
@query(sql="SELECT count(*) as total FROM users")
async def count_users(self) -> int:
...
@query(sql="SELECT u.name, p.title FROM users u JOIN posts p ON u.id = p.user_id WHERE u.id = :uid ORDER BY u.name")
async def get_user_posts(self, uid: int) -> list[dict]:
...
3. Pagination & Sorting¶
Pico-SQLAlchemy provides built-in support for offset-based pagination and dynamic sorting.
Basic Pagination¶
- Set
paged=Truein the@querydecorator. - Add a parameter explicitly named
pageof typePageRequestto your method signature. - Set the return type to
Page[T].
The library automatically generates the count query and applies limits/offsets.
Important: The argument must be named
page. Using other names (e.g.,req,pagination) will result in a runtime error or ignored pagination.
from pico_sqlalchemy import Page, PageRequest
@repository(entity=User)
class UserRepository:
@query(expr="active = true", paged=True)
async def find_active_paged(self, page: PageRequest) -> Page[User]:
...
# Usage: Get page 0, size 10
# await repo.find_active_paged(PageRequest(page=0, size=10))
Dynamic Sorting¶
You can request dynamic sorting by passing a list of Sort objects within the PageRequest.
⚠️ Warning: Expression Mode Only
Dynamic sorting (injecting
ORDER BYclauses based onPageRequest) works only in Expression Mode (expr).If you use SQL Mode (
sql) and provide aPageRequestwith sorts, the library will raise aValueError. This is a security measure to prevent ambiguity and injection risks in raw SQL.
Security Note: To prevent SQL Injection, dynamic sorting in expr mode validates that the requested sort fields exist in the underlying SQLAlchemy model columns. If a field is invalid, a ValueError is raised.
from pico_sqlalchemy import PageRequest, Sort
# Usage: Sort by name ASC, then age DESC
request = PageRequest(
page=0,
size=20,
sorts=[
Sort(field="name", direction="ASC"),
Sort(field="age", direction="DESC")
]
)
# Works automatically because this method uses @query(expr=...)
await repo.find_active_paged(request)
| Parameter | Description |
|---|---|
field | The name of the column in the model (e.g., "username"). |
direction | "ASC" (default) or "DESC". |
4. Return Types & Mapping¶
The @query interceptor is smart about return types:
unique=True: Returns a single scalar/object orNone(usesresult.scalars().first()or mappings).- Default (List): Returns a list of scalars/objects (uses
result.scalars().all()or mappings). - Raw SQL: If the query returns multiple columns (not a mapped entity), it returns dictionary-like mappings.
5. Summary of Behavior¶
| Decorator | Transaction Mode | Execution Logic |
|---|---|---|
@repository (Plain Method) | Read-Write | Executes your Python code body. |
@query | Read-Only | Ignores body; executes SQL/Expr automatically. |
@transactional | Explicit | Executes your Python code body with custom config. |