Advanced Types¶
Specialized SQLAlchemy types for advanced use cases.
MutableList¶
A list type that tracks mutations for proper SQLAlchemy change detection.
Characteristics:
Python type:
listStorage: JSON/JSONB
Mutation tracking: Automatic
Use case: Lists that need in-place modification tracking
Problem Solved¶
Standard Python lists don’t trigger SQLAlchemy change tracking:
from sqlalchemy.orm import Mapped, mapped_column
from advanced_alchemy.base import UUIDBase
from advanced_alchemy.types import JsonB
class TodoList(UUIDBase):
__tablename__ = "todo_lists"
items: "Mapped[list[str]]" = mapped_column(JsonB)
# This won't trigger change detection
todo_list.items.append("New task") # SQLAlchemy doesn't see the change
await session.commit() # Nothing saved!
Solution with MutableList¶
MutableList tracks mutations automatically:
from sqlalchemy.orm import Mapped, mapped_column
from advanced_alchemy.base import UUIDBase
from advanced_alchemy.types import MutableList
class TodoList(UUIDBase):
__tablename__ = "todo_lists"
items: "Mapped[list[str]]" = mapped_column(MutableList)
# Mutations tracked automatically
todo_list.items.append("New task") # Change detected
await session.commit() # Saved correctly
Supported Operations¶
All list mutations are tracked:
# Append
todo_list.items.append("Task 4")
# Extend
todo_list.items.extend(["Task 5", "Task 6"])
# Insert
todo_list.items.insert(0, "Task 0")
# Remove
todo_list.items.remove("Task 2")
# Pop
todo_list.items.pop()
# Index assignment
todo_list.items[0] = "Updated Task"
# Slice assignment
todo_list.items[1:3] = ["New Task 1", "New Task 2"]
# Delete
del todo_list.items[0]
# All trigger change detection
await session.commit()
Complex Data Structures¶
MutableList works with complex nested data:
from typing import Any
from sqlalchemy.orm import Mapped, mapped_column
from advanced_alchemy.base import UUIDBase
from advanced_alchemy.types import MutableList
class Project(UUIDBase):
__tablename__ = "projects"
tasks: "Mapped[list[dict[str, Any]]]" = mapped_column(MutableList)
project.tasks.append({
"title": "Implement feature",
"status": "in_progress",
"assignee": "user@example.com",
"due_date": "2025-10-25",
})
# Update nested value
project.tasks[0]["status"] = "completed"
await session.commit()
When to Use MutableList¶
Use MutableList when:
Lists are modified in-place frequently
Code relies on standard list methods (append, extend, etc.)
Change tracking is critical
Alternative approach (reassignment):
# Without MutableList - use reassignment
items = todo_list.items.copy()
items.append("New task")
todo_list.items = items # Reassignment triggers change detection
await session.commit()
ORA_JSONB¶
Oracle-specific binary JSON type with CHECK constraint.
Characteristics:
Database: Oracle only
Storage: BLOB with JSON CHECK constraint
Use case: Efficient JSON storage on Oracle
Implementation Details¶
ORA_JSONB stores JSON as binary with database-level validation:
from sqlalchemy.orm import Mapped, mapped_column
from advanced_alchemy.base import UUIDBase
from advanced_alchemy.types import ORA_JSONB
class Document(UUIDBase):
__tablename__ = "documents"
# Oracle: BLOB with JSON CHECK constraint
# Other databases: fallback to standard JSON
metadata: "Mapped[dict[str, Any]]" = mapped_column(ORA_JSONB)
Generated SQL (Oracle):
CREATE TABLE documents (
id RAW(16) PRIMARY KEY,
metadata BLOB,
CONSTRAINT metadata_is_json CHECK (metadata IS JSON)
);
Strict vs Non-Strict¶
Control JSON validation strictness:
# Strict validation (default) - rejects duplicates
metadata: "Mapped[dict[str, Any]]" = mapped_column(
ORA_JSONB(oracle_strict=True)
)
# Non-strict - allows duplicate keys
metadata: "Mapped[dict[str, Any]]" = mapped_column(
ORA_JSONB(oracle_strict=False)
)
JsonB vs ORA_JSONB¶
JsonB automatically uses ORA_JSONB on Oracle:
from advanced_alchemy.types import JsonB
# Automatically uses:
# - PostgreSQL: JSONB
# - Oracle: ORA_JSONB (BLOB + CHECK)
# - CockroachDB: JSONB
# - Others: JSON
data: "Mapped[dict[str, Any]]" = mapped_column(JsonB)
Prefer JsonB for cross-database compatibility.
UUID Variants¶
Advanced Alchemy supports UUID v6 and v7 through optional dependencies.
UUID v6 (Sortable UUIDs)¶
Time-ordered UUIDs compatible with UUID v1 but sortable:
from advanced_alchemy.base import UUIDv6Base
class Event(UUIDv6Base):
__tablename__ = "events"
name: "Mapped[str]"
Characteristics:
Time-ordered: Newer UUIDs sort after older ones
Database indexing: Better performance for time-based queries
Installation:
pip install "advanced-alchemy[uuid]"
UUID v7 (Time-Ordered UUIDs)¶
Modern time-ordered UUIDs with millisecond precision:
from advanced_alchemy.base import UUIDv7Base
class LogEntry(UUIDv7Base):
__tablename__ = "log_entries"
message: "Mapped[str]"
Characteristics:
Time-ordered: Timestamp embedded in UUID
Sortable: Natural chronological ordering
Installation:
pip install "advanced-alchemy[uuid]"
NanoID¶
Short, URL-safe unique identifiers as an alternative to UUIDs.
Characteristics:
Length: 21 characters (default)
Character set: URL-safe (alphanumeric plus underscore and hyphen)
Collision resistance: High (comparable to UUID)
Installation:
pip install "advanced-alchemy[nanoid]"
Basic Usage¶
from advanced_alchemy.base import NanoIDBase
class ShortLink(NanoIDBase):
__tablename__ = "short_links"
url: "Mapped[str]"
Example NanoID: V1StGXR8_Z5jdHi6B-myT
When to Use NanoID¶
Use NanoID when:
Shorter IDs needed for URLs
URL-safe characters required
Human-readable IDs preferred over UUIDs
Comparison:
UUID:
550e8400-e29b-41d4-a716-446655440000(36 chars)NanoID:
V1StGXR8_Z5jdHi6B-myT(21 chars)
Type Coercion and Validation¶
Custom Types with Validation¶
Create custom types with validation logic:
from typing import Optional
from sqlalchemy import String
from sqlalchemy.engine import Dialect
from sqlalchemy.types import TypeDecorator
class EmailType(TypeDecorator[str]):
"""Email address type with validation."""
impl = String(255)
cache_ok = True
@property
def python_type(self) -> type[str]:
return str
def process_bind_param(
self,
value: Optional[str],
dialect: Dialect
) -> Optional[str]:
if value is None:
return value
# Basic email validation
if "@" not in value:
raise ValueError(f"invalid email format: {value}")
return value.lower() # Normalize to lowercase
def process_result_value(
self,
value: Optional[str],
dialect: Dialect
) -> Optional[str]:
return value
# Usage
class User(UUIDBase):
__tablename__ = "users"
email: "Mapped[str]" = mapped_column(EmailType)
from sqlalchemy import String
from sqlalchemy.engine import Dialect
from sqlalchemy.types import TypeDecorator
class EmailType(TypeDecorator[str]):
"""Email address type with validation."""
impl = String(255)
cache_ok = True
@property
def python_type(self) -> type[str]:
return str
def process_bind_param(
self,
value: str | None,
dialect: Dialect
) -> str | None:
if value is None:
return value
# Basic email validation
if "@" not in value:
raise ValueError(f"invalid email format: {value}")
return value.lower() # Normalize to lowercase
def process_result_value(
self,
value: str | None,
dialect: Dialect
) -> str | None:
return value
# Usage
class User(UUIDBase):
__tablename__ = "users"
email: "Mapped[str]" = mapped_column(EmailType)
Type Adaptation Pattern¶
Adapt Python types to database-specific types:
from enum import Enum
from typing import Optional
from sqlalchemy import String
from sqlalchemy.engine import Dialect
from sqlalchemy.types import TypeDecorator
class Status(Enum):
PENDING = "pending"
APPROVED = "approved"
REJECTED = "rejected"
class StatusType(TypeDecorator[Status]):
"""Enum type stored as string."""
impl = String(20)
cache_ok = True
@property
def python_type(self) -> type[Status]:
return Status
def process_bind_param(
self,
value: Optional[Status],
dialect: Dialect
) -> Optional[str]:
return value.value if value else None
def process_result_value(
self,
value: Optional[str],
dialect: Dialect
) -> Optional[Status]:
return Status(value) if value else None
# Usage
class Application(UUIDBase):
__tablename__ = "applications"
status: "Mapped[Status]" = mapped_column(StatusType)
from enum import Enum
from sqlalchemy import String
from sqlalchemy.engine import Dialect
from sqlalchemy.types import TypeDecorator
class Status(Enum):
PENDING = "pending"
APPROVED = "approved"
REJECTED = "rejected"
class StatusType(TypeDecorator[Status]):
"""Enum type stored as string."""
impl = String(20)
cache_ok = True
@property
def python_type(self) -> type[Status]:
return Status
def process_bind_param(
self,
value: Status | None,
dialect: Dialect
) -> str | None:
return value.value if value else None
def process_result_value(
self,
value: str | None,
dialect: Dialect
) -> Status | None:
return Status(value) if value else None
# Usage
class Application(UUIDBase):
__tablename__ = "applications"
status: "Mapped[Status]" = mapped_column(StatusType)
Performance Considerations¶
Type Selection Impact¶
Type selection affects database performance:
Type |
Index Performance |
Storage Size |
|---|---|---|
GUID (binary) |
Medium |
16 bytes |
GUID (string) |
Lower |
32-36 bytes |
BigIntIdentity |
High |
8 bytes |
JsonB |
Medium (with indexes) |
Variable |
EncryptedString |
Not indexable |
Variable (larger than plaintext) |
UUID Performance¶
Binary UUID storage is more efficient:
# More efficient
id: "Mapped[UUID]" = mapped_column(GUID(binary=True), primary_key=True)
# Less efficient but more portable
id: "Mapped[UUID]" = mapped_column(GUID(binary=False), primary_key=True)
JSON Indexing¶
PostgreSQL supports JSON indexing:
from sqlalchemy import Index
class UserSettings(UUIDBase):
__tablename__ = "user_settings"
preferences: "Mapped[dict[str, Any]]" = mapped_column(JsonB)
# GIN index for JSONB queries
__table_args__ = (
Index(
"ix_preferences_gin",
"preferences",
postgresql_using="gin"
),
)
Migration Considerations¶
Changing Column Types¶
Be cautious when changing column types in migrations:
# Alembic migration example
def upgrade():
# Add new column
op.add_column("users", sa.Column("email_new", EmailType(), nullable=True))
# Copy and transform data
op.execute("UPDATE users SET email_new = LOWER(email)")
# Drop old column
op.drop_column("users", "email")
# Rename new column
op.alter_column("users", "email_new", new_column_name="email")
See Also¶
Basic Types - Core SQLAlchemy types
Security Types - Encrypted and password types
File Storage - File object storage
types - Complete API reference