Qx database layer: SQLAlchemy 2 async, repositories, unit of work, transactional outbox
Project description
qx-db
Database layer for the Qx framework — SQLAlchemy 2 async, a generic Repository[TEntity], UnitOfWork with domain-event routing, transactional outbox, and cursor/offset pagination.
What lives here
qx.db.Repository[TEntity]— generic async CRUD with optimistic concurrency (versioncolumn), soft-delete, tenant filtering, and allow-listed filter/sort fields.qx.db.UnitOfWork— wraps a SQLAlchemy session; commits the aggregate write and the outboxINSERTin one transaction, then drains and dispatches domain events.qx.db.OutboxRecorder/DefaultOutboxRecorder— persistsIntegrationEventpayloads toqx_outbox_eventsfor reliable delivery.qx.db.SessionFactory— factory forAsyncSessioninstances; injected into repositories.qx.db.make_metadata/make_registry— SQLAlchemyMetaDataandregistryhelpers for imperative mapping (no declarative base).qx.db.standard_audit_columns/uuid_column/jsonb_column— column helpers for consistent schema conventions.qx.db.build_cursor_page/encode_cursor/decode_cursor— opaque cursor pagination utilities.qx.db.include_outbox_table— attaches theqx_outbox_eventstable to yourMetaData.
Defining a repository
from qx.db import Repository
from sqlalchemy import Table
class UserRepository(Repository[User]):
entity_cls = User
table: Table # set to your SQLAlchemy Table at class or instance level
filterable_fields = {"email", "name", "is_active"}
sortable_fields = {"created_at", "email"}
tenanted = False # set True for multi-tenant tables
Unit of Work
from qx.db import UnitOfWork
class CreateUserHandler:
def __init__(self, uow: UnitOfWork) -> None:
self._uow = uow
async def handle(self, cmd: CreateUserCommand) -> Result[UserDto]:
async with self._uow.begin() as ctx:
user_result = User.register(cmd.email, cmd.name)
user = unwrap(user_result)
await ctx.users.add(user)
# commit + outbox INSERT + event dispatch all happened above
return Result.success(UserDto.from_domain(user))
Design rules
- Optimistic concurrency —
save()usesWHERE id = ? AND version = ?; returnsConflictError(not an exception) on mismatch. - Soft-delete by default —
list()andget()exclude rows wheredeleted_at IS NOT NULLunlessinclude_deleted=True. - Allow-listed filters —
filterable_fieldsandsortable_fieldsare class-level sets; querying an unlisted field raisesValueErrorto prevent controller logic leaking into SQL. - Imperative mapping — domain entities are plain dataclasses with no SQLAlchemy decorators. The mapping lives in the infrastructure layer, not the domain.
- UUID v7 —
uuid_column()defaults to UUID v7 primary keys for sequential B-tree index locality.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
qx_db-1.0.0.tar.gz
(23.8 kB
view details)
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
qx_db-1.0.0-py3-none-any.whl
(26.1 kB
view details)
File details
Details for the file qx_db-1.0.0.tar.gz.
File metadata
- Download URL: qx_db-1.0.0.tar.gz
- Upload date:
- Size: 23.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4efae90b24f240eb51532b12ef40ee8391a423efa78999ce66671091f1d2bb81
|
|
| MD5 |
959f11015250ba0bfc5ae5ea38047ab1
|
|
| BLAKE2b-256 |
2731d4901d9d3d2082c49fb06f2f09e270257b9dd180a25af40afe8e0089e2a9
|
File details
Details for the file qx_db-1.0.0-py3-none-any.whl.
File metadata
- Download URL: qx_db-1.0.0-py3-none-any.whl
- Upload date:
- Size: 26.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d9424db56e4a7ea13607aae81cf18b995917139d5257a0409b74f0fabf5aa69
|
|
| MD5 |
e2ef81cb1f7d32f53dfc69934b304a2b
|
|
| BLAKE2b-256 |
5d07a918e368ad59ea06df40b91bc8a8bafc7aea65325f341fb5bca8dc22a9ae
|