Models and field types¶
Declare a database table as a Python class that subclasses Model.
No Column(), no mapped_column(), no SQLAlchemy boilerplate - you describe
what the data is; alchemiq handles how it is stored.
from alchemiq import Model
from alchemiq.types import PK, Email
class User(Model):
id: PK[int]
name: str
email: Email
The table name defaults to the snake_case class name (user).
Override it with an explicit __tablename__ class attribute or via
Meta.table_name.
Field declaration forms¶
Bare annotation¶
Write the field type directly as an annotation. Alchemiq instantiates the type with default settings:
class Article(Model):
id: PK[int]
title: str # plain text column, VARCHAR
email: Email # validated email, VARCHAR(320)
score: NonNegative # int >= 0
Configured value slot¶
Pass a Field (or any field-type instance) on the right-hand side
to customise column behaviour:
from alchemiq import Field, Model
from alchemiq.types import PK, Bounded, Email
class Article(Model):
id: PK[int]
title: str = Field(max_length=200, index=True)
score: int = Bounded(0, 100)
email: Email = Email(unique=True)
Field accepts: nullable, unique, index,
default, server_default, max_length, onupdate.
Any semantic field type (Bounded, Email, etc.) accepts the same
keyword arguments in addition to its own.
Optional fields with Maybe[T]¶
For nullable columns with explicit optional semantics use Maybe[T]:
from alchemiq.types import Maybe, Some, Nothing
class Profile(Model):
id: PK[int]
bio: Maybe[str] # NULL-safe optional
Assign Some(value) or Nothing.
Plain str | None is also accepted for simple cases.
For semantic types prefer Maybe[Email] over Email | None.
Primary keys - PK[T]¶
PK[int] produces a BIGINT autoincrement primary key column named id
(or whatever you call the field):
class Tag(Model):
id: PK[int]
name: str
Alternative PK types are available from alchemiq.types:
Type |
Storage |
Default |
|---|---|---|
|
|
- |
|
PostgreSQL |
|
|
PostgreSQL |
|
|
|
|
A native Mapped[...] = mapped_column(primary_key=True) column
is also recognized as the primary key - see the
native columns guide.
Built-in field types¶
All types live in alchemiq.types.
String types¶
Type |
Validation / behaviour |
Storage |
|---|---|---|
|
Strips, lowercases, validates syntax |
|
|
E.164 format |
|
|
http/https with non-empty host |
|
|
|
|
|
Hashes with scrypt; exposes |
|
Password hashing¶
Password hashes values automatically on write.
The default algorithm is scrypt (Python stdlib - no extra package required).
To switch the global backend, call
configure_password_hashing() once at application startup:
import alchemiq
alchemiq.configure_password_hashing("argon2") # requires pip install "alchemiq[argon2]"
Supported scheme names:
Scheme |
Extra |
Notes |
|---|---|---|
|
(none - stdlib) |
Default |
|
|
|
|
|
Truncates passwords at 72 bytes |
check_password() dispatches on the algorithm prefix embedded
in the stored hash, so it authenticates correctly across schemes.
Hashes stored under the previous algorithm remain valid after a switch;
rows are re-hashed with the new algorithm only when the user next sets a
password, enabling seamless scheme migration without a bulk re-hash.
To restore the default, call reset_password_hashing().
Numeric types¶
Type |
Validation / behaviour |
Storage |
|---|---|---|
|
Inclusive range check |
|
|
>= 1 |
|
|
>= 0 |
|
|
0-100 inclusive |
|
|
Stored as integer minor units, read as |
|
|
Fixed-precision decimal with ROUND_HALF_EVEN |
|
Temporal types¶
Type |
Behaviour |
Storage |
|---|---|---|
|
Timezone-aware datetime |
|
|
Date only |
|
|
Time only |
|
|
Stores as integer, returns |
|
|
Auto-set on insert |
|
|
Auto-updated on every write |
|
Special types¶
Type |
Behaviour |
|---|---|
|
JSONB column with optional Pydantic schema validation |
|
PostgreSQL array with element typing |
|
Transparent AES encryption before write, decryption on read |
|
Python |
Eager validation¶
Every assignment to a field triggers the field type’s validate() method.
This happens at construction time (User(email="bad")) and on bare attribute
assignment (user.email = "bad"):
from alchemiq.exceptions import ValidationError
user = User(id=1, name="Ada", email="not-an-email")
# raises ValidationError immediately
user = User(id=1, name="Ada", email="ada@example.com")
user.email = "bad" # also raises ValidationError
Native Mapped[...] = mapped_column(...) columns are the one exception -
they carry no eager validation (they are an escape hatch whose correctness
is the caller’s responsibility).
The Meta class¶
Place an inner class Meta on your model to configure behaviour flags,
indexes, and constraints:
from alchemiq.model.meta_options import Index, Unique, Check
class Article(Model):
id: PK[int]
slug: str
author_id: int
published_at: DateTimeTz
class Meta:
soft_delete = True # adds deleted_at; enables restore / hard_delete
timestamps = True # adds created_at / updated_at automatically
versioned = True # adds _version; enables optimistic locking -> 409
outbox = True # captures mutations for the transactional outbox
table_name = "articles"
schema = "content"
indexes = [Index("slug", unique=True), Index("author_id")]
constraints = [Unique("slug", "author_id"), Check("published_at IS NOT NULL")]
Behaviour flags:
Flag |
Effect |
|---|---|
|
Injects |
|
Injects |
|
Injects |
|
All mutations are captured for the transactional outbox pattern |
Table configuration:
Option |
Effect |
|---|---|
|
Make a base/mixin model that creates no table of its own (see below) |
|
Override the auto-derived snake_case table name |
|
Place the table in a named PostgreSQL schema |
|
List of |
|
List of |
Abstract base models¶
Set Meta.abstract = True to declare a base (or mixin) model that defines fields
and flags for subclasses to inherit, but maps to no table of its own:
class Auditable(Model):
class Meta:
abstract = True
timestamps = True # inherited by every concrete subclass
class Article(Auditable):
id: PK[int]
title: str
# gets created_at / updated_at from Auditable; only Article maps to a table
Flags and fields declared on an abstract base are inherited by all concrete subclasses; only the concrete subclasses produce database tables.