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.1.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.1.0-py3-none-any.whl
(26.1 kB
view details)
File details
Details for the file qx_db-1.1.0.tar.gz.
File metadata
- Download URL: qx_db-1.1.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 |
577e47a87872a1758469a99f5e2a67731a6a9a1e1dd59c83f83afc056bde8ad5
|
|
| MD5 |
f835013c90445b57eff808c27522abe2
|
|
| BLAKE2b-256 |
d4769630c643c0adc940eb80b732e3f9c267458008d95ab81960673832245234
|
File details
Details for the file qx_db-1.1.0-py3-none-any.whl.
File metadata
- Download URL: qx_db-1.1.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 |
6f00d64a987d4d200b4a829b468a935391d8b7cfc274d29243ec2bc16fe8db69
|
|
| MD5 |
87f7ba19750f1951622d7e2dedb141e4
|
|
| BLAKE2b-256 |
7b1913cee3cc7fc04825c7a05d034dc33f45674fb29647a6660d28e89b6561f0
|