Models¶
Models are the foundation of Oxyde. Each model class represents a database table, with class attributes defining columns.
Basic Model Definition¶
from oxyde import OxydeModel, Field
class User(OxydeModel):
class Meta:
is_table = True
id: int | None = Field(default=None, db_pk=True)
name: str
email: str = Field(db_unique=True)
age: int | None = Field(default=None)
The Meta Class¶
The inner Meta class configures table-level settings:
class User(OxydeModel):
class Meta:
is_table = True # Required: marks this as a database table
table_name = "users" # Optional: custom table name (default: class name)
schema = "public" # Optional: database schema
Required Settings¶
| Setting | Type | Description |
|---|---|---|
is_table |
bool |
Must be True for database tables |
Optional Settings¶
| Setting | Type | Default | Description |
|---|---|---|---|
table_name |
str |
Class name | Database table name |
schema |
str |
None | Database schema |
indexes |
list[Index] |
[] |
Composite indexes |
constraints |
list[Check] |
[] |
CHECK constraints |
unique_together |
list[tuple] |
[] |
Composite unique constraints |
primary_key |
tuple[str, ...] |
None | Composite primary key |
Type Annotations¶
Oxyde uses Python type hints to infer SQL types:
class Example(OxydeModel):
class Meta:
is_table = True
# Required field
name: str
# Optional field (nullable)
bio: str | None = Field(default=None)
# With default value
status: str = Field(default="active")
Type Mapping¶
| Python Type | PostgreSQL | SQLite | MySQL |
|---|---|---|---|
int |
BIGINT | INTEGER | BIGINT |
str |
TEXT | TEXT | TEXT |
float |
DOUBLE PRECISION | REAL | DOUBLE |
bool |
BOOLEAN | INTEGER | TINYINT |
datetime |
TIMESTAMP | TEXT | DATETIME |
date |
DATE | TEXT | DATE |
UUID |
UUID | TEXT | CHAR(36) |
Decimal |
NUMERIC | NUMERIC | DECIMAL |
bytes |
BYTEA | BLOB | BLOB |
Primary Keys¶
Auto-increment Primary Key¶
class User(OxydeModel):
class Meta:
is_table = True
id: int | None = Field(default=None, db_pk=True)
The id will be auto-generated on insert.
UUID Primary Key¶
from uuid import UUID, uuid4
class User(OxydeModel):
class Meta:
is_table = True
id: UUID = Field(default_factory=uuid4, db_pk=True)
Composite Primary Key¶
class UserRole(OxydeModel):
class Meta:
is_table = True
primary_key = ("user_id", "role_id")
user_id: int
role_id: int
Indexes¶
Single-Column Index¶
Composite Index¶
from oxyde import Index
class Event(OxydeModel):
class Meta:
is_table = True
indexes = [
Index(("city", "start_date")),
]
city: str
start_date: datetime
Partial Index¶
class User(OxydeModel):
class Meta:
is_table = True
indexes = [
Index(("email",), unique=True, where="deleted_at IS NULL"),
]
email: str
deleted_at: datetime | None = Field(default=None)
Index Methods¶
PostgreSQL supports different index methods:
Index(("data",), method="gin") # GIN index for JSONB
Index(("name",), method="hash") # Hash index for equality
Constraints¶
UNIQUE Constraint¶
# Single column
email: str = Field(db_unique=True)
# Multiple columns
class Meta:
unique_together = [("user_id", "slug")]
CHECK Constraint¶
from oxyde import Check
class Event(OxydeModel):
class Meta:
is_table = True
constraints = [
Check("start_date < end_date", name="valid_dates"),
Check("price >= 0"),
]
start_date: datetime
end_date: datetime
price: float
SQL Defaults¶
Set database-level default values:
class User(OxydeModel):
class Meta:
is_table = True
id: int | None = Field(default=None, db_pk=True)
created_at: datetime = Field(db_default="CURRENT_TIMESTAMP")
uuid: str = Field(db_default="gen_random_uuid()") # PostgreSQL
status: str = Field(db_default="'active'") # Note: strings need quotes
Python vs SQL Defaults
default=value— Python-side default, used when creating instancesdb_default="..."— SQL-side default, used by the database
Column Mapping¶
Override the database column name:
class User(OxydeModel):
class Meta:
is_table = True
created_at: datetime = Field(db_column="created_timestamp")
The Python attribute is created_at, but the database column is created_timestamp.
Custom SQL Types¶
Override the inferred SQL type:
class User(OxydeModel):
class Meta:
is_table = True
id: int = Field(db_pk=True, db_type="BIGSERIAL")
name: str = Field(db_type="VARCHAR(255)")
data: dict = Field(db_type="JSONB") # PostgreSQL
Instance Methods¶
save()¶
Insert or update a record:
# Insert new record
user = User(name="Alice", email="alice@example.com")
await user.save()
print(user.id) # Auto-generated ID
# Update existing record
user.name = "Alice Smith"
await user.save()
# Partial update (only specified fields)
user.age = 31
await user.save(update_fields=["age"])
delete()¶
Delete a record:
refresh()¶
Reload from database:
user = await User.objects.get(id=1)
# ... some other process updates the database ...
await user.refresh() # Reload latest data
Lifecycle Hooks¶
Override these methods to run code before/after database operations:
class User(OxydeModel):
class Meta:
is_table = True
id: int | None = Field(default=None, db_pk=True)
name: str
email: str
created_at: datetime | None = Field(default=None)
updated_at: datetime | None = Field(default=None)
async def pre_save(self, *, is_create: bool, update_fields: list[str] | None = None):
"""Called before save()."""
from datetime import datetime
now = datetime.utcnow()
if is_create:
self.created_at = now
self.updated_at = now
async def post_save(self, *, is_create: bool, update_fields: list[str] | None = None):
"""Called after save()."""
if is_create:
print(f"Created user {self.id}")
async def pre_delete(self):
"""Called before delete()."""
print(f"About to delete user {self.id}")
async def post_delete(self):
"""Called after delete()."""
print(f"Deleted user {self.id}")
Model Inheritance¶
Abstract Models¶
Create base models without database tables:
class TimestampMixin(OxydeModel):
"""Mixin for created_at/updated_at fields."""
created_at: datetime = Field(db_default="CURRENT_TIMESTAMP")
updated_at: datetime | None = Field(default=None)
class User(TimestampMixin):
class Meta:
is_table = True
id: int | None = Field(default=None, db_pk=True)
name: str
Only User creates a database table.
Pydantic Integration¶
OxydeModel inherits from Pydantic's BaseModel, so you get:
Validation¶
class User(OxydeModel):
class Meta:
is_table = True
id: int | None = Field(default=None, db_pk=True)
age: int = Field(ge=0, le=150) # Must be 0-150
email: str = Field(pattern=r"^[\w.-]+@[\w.-]+\.\w+$")
# Raises ValidationError
user = User(age=200, email="invalid")
Serialization¶
user = await User.objects.get(id=1)
# To dict
data = user.model_dump()
# To JSON
json_str = user.model_dump_json()
# From dict
user = User.model_validate({"name": "Alice", "email": "alice@example.com"})
JSON Aliases¶
class User(OxydeModel):
class Meta:
is_table = True
created_at: datetime = Field(
alias="createdAt", # JSON key
db_column="created_at", # Database column
)