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. |
Auto-generated API¶
pico_sqlalchemy.repository_interceptor ¶
AOP interceptor that executes declarative @query methods.
RepositoryQueryInterceptor is the second link in the interceptor chain for @query-decorated methods. By the time it runs, the TransactionalInterceptor has already ensured that a transaction (and therefore an AsyncSession) is active.
Two execution modes are supported:
-
Expression mode (
@query(expr="...")) -- generatesSELECT * FROM <table> WHERE <expr>using the entity's__tablename__. Dynamic sorting viaPageRequest.sortsis validated against the entity's column set to prevent injection. -
SQL mode (
@query(sql="...")) -- executes the raw SQL string verbatim. Dynamic sorting is not supported in this mode to prevent SQL injection.
Both modes support pagination (paged=True) with automatic COUNT(*) and LIMIT/OFFSET handling.
RepositoryQueryInterceptor ¶
Bases: MethodInterceptor
Executes declarative queries for @query-decorated methods.
This interceptor runs after TransactionalInterceptor in the AOP chain. It checks for QUERY_META on the target method; if absent, it simply delegates to call_next. When present, the method body is never executed -- instead the interceptor builds and runs the SQL query, binding method parameters automatically.
Two execution modes:
expr-- generatesSELECT * FROM <table> WHERE <expr>using the entity from@repository(entity=...).sql-- executes the provided raw SQL verbatim.
Both modes support pagination (paged=True) and unique result (unique=True).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session_manager | SessionManager | The | required |
Source code in src/pico_sqlalchemy/repository_interceptor.py
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 | |
invoke(ctx, call_next) async ¶
Intercept the method and execute the declarative query if applicable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ctx | MethodCtx | The AOP method context. | required |
call_next | Callable[[MethodCtx], Any] | Callback to the next interceptor or the original method. | required |
Returns:
| Type | Description |
|---|---|
Any | Query results -- a |
Any | single |
Raises:
| Type | Description |
|---|---|
RuntimeError |
|
TypeError |
|
ValueError |
|
ValueError |
|