Database¶
commons.database
¶
SQLModel/SQLAlchemy building blocks shared across apps.
Reusable persistence primitives that sit below any one app's ORM models:
- Ids (:func:
new_id, from :mod:commons.id_operators) — time-ordered UUIDv7 strings for new rows. - Column length bounds (:data:
ID_LENGTH, :data:CODE_LENGTH, :data:NAME_LENGTH) — pick a bound so String columns stay indexable on SQL Server. - Column types (:class:
EnumString, :class:PydanticJSON, :class:UTCDateTime, :class:UnicodeCode, :class:UnicodeName) —TypeDecorators handed toField(sa_type=...). - Table mixins (:class:
EntityMixin, :class:HistoryMixin) — a UUIDv7-PK operational entity base and its temporal history-table companion. - Unit-of-work ports (:class:
UnitOfWork, :class:UnitOfWorkFactory) — abstract transactional-scope contracts, generic over an app-defined repository bundleT. - SQL unit of work (:class:
SqlUnitOfWork, :class:SqlUnitOfWorkFactory) — aSession-per-scope implementation of those ports; the request actor is injected (current_user_provider) so the library stays free of any web-framework dependency.
These are engine-portable (SQLite for tests, SQL Server / Azure SQL in production); the SQL-Server-only pieces (filtered indexes) are gated by dialect.
Text columns on SQL Server. SQLModel makes str columns VARCHAR(n). With the default collation of
Azure SQL (code page 1252), VARCHAR keeps German umlauts but stores other characters (ł, ş, Cyrillic,
Greek, emoji) as ?, silently. Use VARCHAR (plain Field(max_length=...)) for ASCII values: ids, codes,
hashes, e-mail addresses, URLs, keys. Use NVARCHAR (:class:UnicodeCode, :class:UnicodeName, or
Unicode(n) in a Column) for names and free text people enter. create_all doesn't change existing
tables; convert a column with e.g. ALTER TABLE registered_users ALTER COLUMN display_name NVARCHAR(128) NOT NULL
(keep the column's length and nullability, and drop and recreate an index on it first).
Modules:
| Name | Description |
|---|---|
column_lengths |
Shared |
entity |
|
history |
|
types |
SQLModel-compatible column types ( |
unit_of_work |
|
unit_of_work_sql |
|
Classes:
| Name | Description |
|---|---|
EntityMixin |
Base for an operational entity table: a generated |
HistoryMixin |
Base for a history table, generic over the entity ORM |
EnumString |
A length-bounded |
PydanticJSON |
JSON column that round-trips any Pydantic-validatable type |
UnicodeCode |
A length-bounded Unicode column ( |
UnicodeName |
A length-bounded Unicode column ( |
UTCDateTime |
Store tz-aware datetimes as naive UTC (engine-portable). Read back naive (repo convention). |
UnitOfWork |
Port for a single transactional scope (one |
UnitOfWorkFactory |
Port that mints a fresh :class: |
SqlUnitOfWork |
One |
SqlUnitOfWorkFactory |
Mints a fresh :class: |
Functions:
| Name | Description |
|---|---|
new_id |
Return a new id: a time-ordered UUIDv7 as a 36-character string. |
Attributes:
| Name | Type | Description |
|---|---|---|
CODE_LENGTH |
|
|
ID_LENGTH |
|
|
NAME_LENGTH |
|
|
CommitFunction |
|
__all__
module-attribute
¶
__all__ = [
"CODE_LENGTH",
"ID_LENGTH",
"NAME_LENGTH",
"CommitFunction",
"EntityMixin",
"EnumString",
"HistoryMixin",
"PydanticJSON",
"SqlUnitOfWork",
"SqlUnitOfWorkFactory",
"UTCDateTime",
"UnicodeCode",
"UnicodeName",
"UnitOfWork",
"UnitOfWorkFactory",
"new_id",
]
EntityMixin
¶
Bases: SQLModel
Base for an operational entity table: a generated id primary key and a derived table name.
Inheriting it (with table=True) gives the table a UUIDv7-string id PK (commons.id_operators.new_id) and a name derived from
the class (CountryORM -> countries) via declared_attr (so it runs at class-creation time).
Override the name by setting __tablename__ explicitly on the subclass.
Methods:
| Name | Description |
|---|---|
__tablename__ |
|
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
|
HistoryMixin
¶
Bases: SQLModel
Base for a history table, generic over the entity ORM E it mirrors (HistoryMixin[CountryORM]).
Inheriting it (with table=True) derives the table name from the class and adds the open-interval
index — both via declared_attr so they run at class-creation time. Override the name by setting
__tablename__ explicitly on the subclass.
Methods:
| Name | Description |
|---|---|
from_entity |
Open a new history row mirroring |
close_open_interval |
Close this entity's open interval ( |
at_timestamp |
The version of |
all_at_timestamp |
Every entity's version valid at |
record |
Append a new version: close the previous open interval, then add an open row mirroring |
__tablename__ |
|
__table_args__ |
|
Attributes:
| Name | Type | Description |
|---|---|---|
id |
str
|
|
history_valid_from |
datetime
|
|
history_valid_to |
datetime | None
|
|
history_changed_by_user_displayname |
str
|
|
history_changed_by_user_id |
str
|
|
history_deleted |
bool
|
|
history_valid_from
class-attribute
instance-attribute
¶
history_valid_from: datetime = Field(
primary_key=True, sa_type=UTCDateTime
)
history_valid_to
class-attribute
instance-attribute
¶
history_valid_to: datetime | None = Field(
default=None, sa_type=UTCDateTime
)
history_changed_by_user_displayname
class-attribute
instance-attribute
¶
history_changed_by_user_displayname: str = Field(
sa_type=UnicodeName
)
history_changed_by_user_id
class-attribute
instance-attribute
¶
history_changed_by_user_id: str = Field(
max_length=NAME_LENGTH
)
from_entity
classmethod
¶
from_entity(
user: User,
entity: E,
timestamp: datetime,
*,
deleted: bool = False
) -> Self
Open a new history row mirroring entity's columns, stamped with the actor and time.
Copies the columns the entity and this history table share (by name); entity-only columns (e.g. an operational soft-delete flag) are ignored.
Source code in src/commons/database/history.py
close_open_interval
classmethod
¶
close_open_interval(
session: Session,
entity_id: str,
timestamp: datetime | None = None,
) -> datetime
Close this entity's open interval (history_valid_to IS NULL), if any, at timestamp.
Returns the timestamp used, so the caller can reuse it as the new version's history_valid_from
(giving contiguous half-open intervals [from, to)).
Source code in src/commons/database/history.py
at_timestamp
classmethod
¶
at_timestamp(
session: Session,
entity_id: str,
timestamp: datetime,
*,
include_deleted: bool = False
) -> Self | None
The version of entity_id valid at timestamp (half-open [from, to)), or None.
Intervals never overlap, so at most one row matches. A tombstone (history_deleted) counts
as "did not exist" and yields None by default; pass include_deleted=True to get the
tombstone row itself (e.g. to show "deleted at …").
Source code in src/commons/database/history.py
all_at_timestamp
classmethod
¶
all_at_timestamp(
session: Session,
timestamp: datetime,
*,
include_deleted: bool = False
) -> Sequence[Self]
Every entity's version valid at timestamp (half-open [from, to)).
The set-valued companion to :meth:at_timestamp: one row per entity that
existed at timestamp (tombstones excluded unless include_deleted).
Backs cross-aggregate reverse lookups that must run against a historical
snapshot rather than the live tables.
Source code in src/commons/database/history.py
record
classmethod
¶
record(
session: Session,
user: User,
entity: E,
*,
timestamp: datetime | None = None,
deleted: bool = False
) -> Self
Append a new version: close the previous open interval, then add an open row mirroring entity.
The single per-entity write path — it always closes before opening, so the "at most one open
interval per id" invariant can't be broken by forgetting a step. timestamp defaults to now;
pass one to backdate or to align several entities to the same instant. Returns the added row.
Source code in src/commons/database/history.py
__tablename__
¶
__table_args__
¶
Source code in src/commons/database/history.py
EnumString
¶
Bases: TypeDecorator[str]
A length-bounded String for Literal-valued columns (status / kind / mode / license).
A TypeDecorator rather than String(ENUM_LENGTH) so it can be handed to
SQLModel's sa_type as a type (which SQLModel instantiates into a
VARCHAR(ENUM_LENGTH) column) instead of an instance — SQLModel types sa_type
as type[Any], so passing an instance fails type checking.
Attributes:
| Name | Type | Description |
|---|---|---|
impl |
|
|
cache_ok |
|
PydanticJSON
¶
Bases: TypeDecorator[T], Generic[T]
JSON column that round-trips any Pydantic-validatable type T via a TypeAdapter.
T can be a model, list[Model], dict[str, Model], etc. Writes dump to
JSON-native values (so the JSON impl can encode them); reads rebuild validated
instances rather than handing back raw dicts.
Methods:
| Name | Description |
|---|---|
process_bind_param |
|
process_result_value |
|
Attributes:
| Name | Type | Description |
|---|---|---|
impl |
|
|
cache_ok |
|
|
data_type |
|
Source code in src/commons/database/types.py
process_bind_param
¶
Source code in src/commons/database/types.py
UnicodeCode
¶
Bases: TypeDecorator[str]
A length-bounded Unicode column (CODE_LENGTH) for short text people enter, e.g. a case number.
NVARCHAR(64) on SQL Server, where VARCHAR stores only code page 1252 and turns other characters into
?; VARCHAR(64) elsewhere. Hand it to Field(sa_type=...) as a type, like EnumString
(sa_type=Unicode would ignore max_length and give an unindexable NVARCHAR(max)).
Attributes:
| Name | Type | Description |
|---|---|---|
impl |
|
|
cache_ok |
|
UnicodeName
¶
UTCDateTime
¶
Bases: TypeDecorator[datetime]
Store tz-aware datetimes as naive UTC (engine-portable). Read back naive (repo convention).
Methods:
| Name | Description |
|---|---|
process_bind_param |
|
Attributes:
| Name | Type | Description |
|---|---|---|
impl |
|
|
cache_ok |
|
process_bind_param
¶
UnitOfWork
¶
Bases: ABC
Port for a single transactional scope (one with block).
Entering opens a transaction and returns the repository bundle plus a
commit callable; leaving the block rolls back unless commit was
awaited. Generic over T - the app-defined bundle of repositories the
scope exposes.
Methods:
| Name | Description |
|---|---|
__enter__ |
|
__exit__ |
|
__enter__
abstractmethod
¶
__enter__() -> tuple[T, CommitFunction]
UnitOfWorkFactory
¶
Bases: ABC
Port that mints a fresh :class:UnitOfWork per call - safe to share process-wide.
for_user opens a unit of work attributed to the request user; for_system one
attributed to the system user (background / bulk work). Each call yields its own
session/scope, so concurrent requests never share transactional state or audit actor.
The factory itself is not a context manager - always go through one of these methods so
the object you enter is the object you exit.
Methods:
| Name | Description |
|---|---|
for_user |
Open a unit of work attributed to the request user (sets the audit actor). |
for_system |
Open a unit of work with history writing off (bulk seeding). |
__enter__ |
|
__exit__ |
|
for_user
abstractmethod
¶
for_user(user: User) -> UnitOfWork[T]
for_system
abstractmethod
¶
for_system() -> UnitOfWork[T]
__enter__
abstractmethod
¶
__enter__() -> tuple[T, CommitFunction]
SqlUnitOfWork
¶
SqlUnitOfWork(
session_factory: sessionmaker,
repositories_factory: Callable[[Session, User], T],
*,
user: User
)
Bases: UnitOfWork[T]
One Session and one transaction for a single with block.
A fresh instance per with (minted by :class:SqlUnitOfWorkFactory), so its
session and audit actor are never shared between concurrent requests - commit and
__exit__ always act on this block's own session. __enter__ opens the session
and returns the repository bundle plus an async commit; __exit__ rolls back
(if commit was never awaited) and closes.
Methods:
| Name | Description |
|---|---|
__enter__ |
|
__exit__ |
|
Source code in src/commons/database/unit_of_work_sql.py
__enter__
¶
__enter__() -> tuple[T, CommitFunction]
SqlUnitOfWorkFactory
¶
SqlUnitOfWorkFactory(
session_factory: sessionmaker,
repositories_factory: Callable[[Session, User], T],
current_user_provider: Callable[
[], User
] = _system_user,
)
Bases: UnitOfWorkFactory[T]
Mints a fresh :class:SqlUnitOfWork per with block.
The factory is stateless and safe to share process-wide (it lives on the single
composition root): for_user / for_system each return a new unit of work, so the
object you enter is the object you exit. No per-request state lives on the shared
instance, so concurrent requests cannot clobber each other's session or audit actor.
Use with uow.for_user(user) as (repos, commit) so every history row is attributed;
for_system() turns history off (bulk seeding). Entering the factory itself as a
context manager resolves the actor via current_user_provider - inject one that reads
your web framework's request context (the default attributes work to the system user).
Keeping actor resolution injected is deliberate: it keeps this library free of any
web-framework dependency.
Methods:
| Name | Description |
|---|---|
for_user |
A unit of work attributed to |
for_system |
A unit of work with history writing off (bulk seeding). |
__enter__ |
|
__exit__ |
|
Source code in src/commons/database/unit_of_work_sql.py
for_user
¶
for_user(user: User) -> SqlUnitOfWork[T]
for_system
¶
for_system() -> SqlUnitOfWork[T]
__enter__
¶
__enter__() -> tuple[T, CommitFunction]
new_id
¶
Return a new id: a time-ordered UUIDv7 as a 36-character string.
Ids created one after another in a process compare in creation order, so as primary keys they are appended to the index instead of landing anywhere in it. They reveal when they were created; don't use them as secrets.
Returns:
| Type | Description |
|---|---|
str
|
The id, e.g. |