Mixins¶
Schemap ships with nine reusable mixins for common patterns.
TimestampMixin¶
Adds created_at and updated_at with auto-populating defaults.
from schemap import TimestampMixin, AutoBase
class Post(AutoBase, TimestampMixin):
__tablename__ = "posts"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str]
Both columns use default=lambda: datetime.now(timezone.utc). updated_at also uses onupdate to refresh on every write. Both are excluded from CreateSchema (they have client defaults).
SoftDeleteMixin¶
Adds deleted_at timestamp, soft_delete(), and active() filter.
from schemap import SoftDeleteMixin, AutoBase
class Article(AutoBase, SoftDeleteMixin):
__tablename__ = "articles"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str]
article.soft_delete()
# article.deleted_at is now set
# Query for undeleted records
from sqlalchemy import select
results = session.execute(
select(Article).where(Article.active())
).scalars().all()
CreatedByMixin and UpdatedByMixin¶
Audit trail mixins with foreign keys and relationships to users.
from schemap import CreatedByMixin, UpdatedByMixin, AutoBase
class Document(AutoBase, CreatedByMixin, UpdatedByMixin):
__tablename__ = "documents"
id: Mapped[int] = mapped_column(primary_key=True)
content: Mapped[str]
CreatedByMixin adds created_by_id (FK) and created_by (relationship). UpdatedByMixin adds updated_by_id and updated_by. Use separately or together.
StatusMixin¶
Adds status column with activate() and deactivate() methods.
from schemap import StatusMixin, AutoBase
class Subscription(AutoBase, StatusMixin):
__tablename__ = "subscriptions"
id: Mapped[int] = mapped_column(primary_key=True)
plan: Mapped[str]
sub.activate() # status = "active"
sub.deactivate() # status = "inactive"
Default status is "active".
ArchivableMixin¶
Adds archived_at timestamp with archive() and restore() methods.
from schemap import ArchivableMixin, AutoBase
class Order(AutoBase, ArchivableMixin):
__tablename__ = "orders"
id: Mapped[int] = mapped_column(primary_key=True)
total: Mapped[float]
order.archive() # archived_at is set
order.restore() # archived_at is None
VersionMixin¶
Adds version: int column for optimistic locking.
from schemap import VersionMixin, AutoBase
class Product(AutoBase, VersionMixin):
__tablename__ = "products"
id: Mapped[int] = mapped_column(primary_key=True)
price: Mapped[float]
product.increment_version()
UUIDPrimaryKeyMixin and IntPrimaryKeyMixin¶
Standard primary key columns.
from schemap import UUIDPrimaryKeyMixin, IntPrimaryKeyMixin, AutoBase
class Tenant(AutoBase, UUIDPrimaryKeyMixin):
__tablename__ = "tenants"
name: Mapped[str]
class Tag(AutoBase, IntPrimaryKeyMixin):
__tablename__ = "tags"
name: Mapped[str]
UUIDPrimaryKeyMixin uses default=uuid4. IntPrimaryKeyMixin provides a bare int PK.
Writing your own mixin¶
A mixin is a plain class with Mapped columns. No base class needed.
from sqlalchemy.orm import Mapped, mapped_column
class AuditMixin:
created_by: Mapped[str] = mapped_column(nullable=False)
updated_by: Mapped[str] = mapped_column(nullable=True)
class Document(AutoBase, AuditMixin):
__tablename__ = "documents"
id: Mapped[int] = mapped_column(primary_key=True)
content: Mapped[str]
Guidelines:
- Column names must not collide between mixins or with the model.
- Do not define __tablename__ in a mixin.
- Use Optional or | None for nullable columns.
- Add methods for domain logic (activate, archive, soft_delete, etc.).