ORM¶
Defining Tables¶
from derp.orm import (
Table, Field, Fn, FK, Nullable, UUID, Varchar, Text,
Integer, Boolean, TimestampTZ,
)
class Product(Table, table="products"):
id: UUID = Field(primary=True, default=Fn.gen_random_uuid())
name: Varchar[255] = Field()
description: Nullable[Text] = Field()
price: Integer = Field()
is_active: Boolean = Field(default=True)
seller_id: UUID = Field(foreign_key=User.id, on_delete=FK.CASCADE)
created_at: TimestampTZ = Field(default=Fn.now())
The column type is the annotation (e.g. Varchar[255]), not a Field()
argument. Use Nullable[Type] for nullable columns. Access columns via
Product.name, Product.price, etc.
Field Types¶
Annotation |
SQL Type |
|---|---|
|
UUID |
|
VARCHAR(255) |
|
TEXT |
|
INTEGER |
|
BIGINT |
|
SMALLINT |
|
SERIAL |
|
BOOLEAN |
|
TIMESTAMPTZ |
|
TIMESTAMP |
|
DATE |
|
JSON |
|
JSONB |
|
NUMERIC |
|
(derived from class name) |
|
(adds NULL) |
Foreign Keys¶
from derp.orm import FK
seller_id: UUID = Field(foreign_key=User.id, on_delete=FK.CASCADE)
FK members: CASCADE, SET_NULL, SET_DEFAULT, RESTRICT.
Lowercase strings ("cascade", etc.) are also accepted.
SQL Functions (Fn)¶
Use the Fn enum for predefined SQL defaults instead of magic strings:
from derp.orm import Fn
id: UUID = Field(primary=True, default=Fn.gen_random_uuid())
created_at: TimestampTZ = Field(default=Fn.now())
Functions: gen_random_uuid(), now(), current_timestamp().
Fn.to_tsvector(config, *columns) builds a tsvector expression:
Fn.to_tsvector("english", "title", "body")
# → to_tsvector('english', title || ' ' || body)
Generated Columns¶
Use generated to create columns computed by PostgreSQL on every write.
generated and default are mutually exclusive.
class OrderLine(Table, table="order_lines"):
price: Integer = Field()
quantity: Integer = Field()
amount: Integer = Field(generated="price * quantity")
This produces amount INTEGER GENERATED ALWAYS AS (price * quantity) STORED.
A common use case is full-text search vectors:
from derp.orm import Fn
class Article(Table, table="articles"):
title: Varchar[255] = Field()
body: Text = Field()
search_vector: TSVector = Field(
generated=Fn.to_tsvector("english", "title", "body")
)
Select¶
db = derp.db
# Basic select
products = await db.select(Product).execute()
# Filtering
product = await (
db.select(Product)
.where(Product.id == product_id)
.first_or_none()
)
# Operators
results = await (
db.select(Product)
.where(Product.price > 1000)
.where(Product.name.ilike("%phone%"))
.where(Product.seller_id.in_([id1, id2]))
.order_by(Product.price, asc=False)
.limit(20)
.offset(40)
.execute()
)
Available filter methods on columns: ==, !=, >, >=,
<, <=, .in_(), .not_in(), .like(), .ilike(),
.is_null(), .is_not_null(), .between().
Joins¶
orders = await (
db.select(Order, User.email)
.from_(Order)
.inner_join(User, Order.user_id == User.id)
.where(Order.total > 5000)
.execute()
)
Join methods: inner_join, left_join, right_join,
full_join, cross_join.
Insert¶
product = await (
db.insert(Product)
.values(name="Laptop", price=99900, seller_id=user_id)
.returning(Product)
.execute()
)
# Bulk insert (no returning)
await (
db.insert(Product)
.values_list([
{"name": "Mouse", "price": 2500, "seller_id": user_id},
{"name": "Keyboard", "price": 7500, "seller_id": user_id},
])
.execute()
)
Update¶
updated = await (
db.update(Product)
.set(price=89900, is_active=False)
.where(Product.id == product_id)
.returning(Product)
.execute()
)
Delete¶
await (
db.delete(Product)
.where(Product.id == product_id)
.execute()
)
Transactions¶
async with db.transaction() as txn:
order = await (
txn.insert(Order)
.values(user_id=user_id, total=9900)
.returning(Order)
.execute()
)
await (
txn.insert(OrderItem)
.values(order_id=order.id, product_id=pid, qty=1)
.execute()
)
Rolls back automatically on exception, commits on clean exit.
Raw SQL¶
from derp.orm import sql
results = await (
db.select(Product.name, sql("UPPER(name)").as_("upper_name"))
.from_(Product)
.execute()
)
# With parameters
rows = await db.execute(
"SELECT * FROM products WHERE price > $1", [5000]
)
Aggregates¶
stats = await (
db.select(
Product.price.sum().as_("total"),
Product.price.avg().as_("average"),
Product.id.count().as_("count"),
Product.price.min().as_("cheapest"),
Product.price.max().as_("priciest"),
)
.from_(Product)
.execute()
)
Group By / Having¶
sales = await (
db.select(Product.seller_id, Product.price.sum().as_("revenue"))
.from_(Product)
.group_by(Product.seller_id)
.having(Product.price.sum() > 100000)
.execute()
)
Subqueries and EXISTS¶
# Subquery in WHERE
active_sellers = db.select(User.id).where(User.is_active)
products = await (
db.select(Product)
.where(Product.seller_id.in_(active_sellers))
.execute()
)
# EXISTS
has_orders = db.select(Order).where(Order.user_id == User.id)
users = await (
db.select(User)
.where(has_orders.exists())
.execute()
)
Set Operations¶
recent = db.select(Product).where(Product.created_at > cutoff)
popular = db.select(Product).where(Product.sales > 100)
combined = await recent.union(popular).execute()
overlap = await recent.intersect(popular).execute()
only_new = await recent.except_(popular).execute()