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-0.2.0.tar.gz
(21.9 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-0.2.0-py3-none-any.whl
(24.5 kB
view details)
File details
Details for the file qx_db-0.2.0.tar.gz.
File metadata
- Download URL: qx_db-0.2.0.tar.gz
- Upload date:
- Size: 21.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
528aac6a74334e7bd57b29ea4b9766a4ef2c1403ec2d84e2fa0b7e8a440aed4e
|
|
| MD5 |
c2a5f3e31ee78abd6e93a0abb87e8626
|
|
| BLAKE2b-256 |
16c37c6b7e21bf90f44384726a93357a953a122fc7f662c8c1e1c3ed92a199f5
|
File details
Details for the file qx_db-0.2.0-py3-none-any.whl.
File metadata
- Download URL: qx_db-0.2.0-py3-none-any.whl
- Upload date:
- Size: 24.5 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 |
b33bf53d3c380af2486d2942fd5018af3ee15d4776eefafe4a4879bb0201e862
|
|
| MD5 |
18f619028f53cad08e100fdaecdd0b14
|
|
| BLAKE2b-256 |
3d53cfc8cc9306902d676e72f2f7252dbdb33780a3d1ad8115dd2c4266c1ef51
|