Typed JSON fields for SQLAlchemy with automatic mutation tracking
Project description
sqlatypemodel
Typed JSON fields for SQLAlchemy with automatic mutation tracking.
SQLAlchemy does not detect in-place changes inside JSON columns by default. sqlatypemodel fixes this, enabling you to work with fully typed Python objects (Pydantic, Dataclasses, Attrs, or custom classes) while ensuring all changes are automatically saved to the database.
Key Features
-
Seamless Integration: Store Pydantic models directly in SQLAlchemy columns.
-
Universal Support: Works with Pydantic (V1 & V2), Dataclasses, Attrs, and custom classes.
-
Mutation Tracking: Built-in
MutableMixindetects deep changes (e.g.,user.data.list.append("item")) and flags the row for update. -
High Performance:
-
O(1) Wrapping: Smart "short-circuit" logic prevents re-wrapping already tracked collections.
-
Optimized Updates: Avoids expensive serialization (
model_dump) on every attribute change, using fast identity checks instead. -
Automatic Serialization: Handles conversion to/from JSON automatically.
The Problem
By default, SQLAlchemy considers JSON columns immutable unless you replace the entire object.
# ❌ NOT persisted by default SQLAlchemy
user.settings.theme = "dark"
user.tags.append("new")
session.commit() # Nothing happens!
The Solution
With sqlatypemodel, in-place mutations are tracked:
# ✅ Persisted automatically
user.settings.theme = "dark"
user.tags.append("new")
session.commit() # UPDATE "users" SET ...
Installation
pip install sqlatypemodel
Performance Benchmarks
sqlatypemodel is designed for high-load production environments. We benchmarked assignment operations to ensure minimal overhead.
Test Scenario: Assigning a pre-filled list of 100,000 integers to a model field.
| Operation | Complexity | Time (100k items) | Notes |
|---|---|---|---|
| Naive Re-wrapping | O(N) | ~0.15s+ | Recursively traversing and wrapping every item. |
| sqlatypemodel | Optimized | <0.01s | Uses identity checks to skip re-wrapping known collections. |
| Change Detection | O(1) | Instant | Uses id() comparison instead of deep equality checks. |
Benchmarks run on Python 3.12, Pydantic V2.
Quick Start (Pydantic)
This is the most common use case. MutableMixin and ModelType work together to handle everything.
from typing import List
from pydantic import BaseModel
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
from sqlatypemodel import ModelType, MutableMixin
# 1. Define your Pydantic Model
# Note: MutableMixin MUST be the first parent class.
class UserSettings(MutableMixin, BaseModel):
theme: str = "light"
notifications: bool = True
tags: List[str] = []
# 2. Define SQLAlchemy Entity
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
# 3. Use ModelType
settings: Mapped[UserSettings] = mapped_column(ModelType(UserSettings))
# 4. Usage
engine = create_engine("sqlite:///")
Base.metadata.create_all(engine)
with Session(engine) as session:
user = User(settings=UserSettings())
session.add(user)
session.commit()
# --- Mutation Tracking ---
# Modify fields directly:
user.settings.theme = "dark"
# Modify nested collections:
user.settings.tags.append("python")
session.commit() # Changes are saved automatically
Advanced Usage
sqlatypemodel is not limited to Pydantic. You can use it with any class by providing json_dumps and json_loads (or by implementing to_json/from_json methods).
Python Dataclasses
Standard dataclasses are supported, but you must enable identity hashing (__hash__ = object.__hash__) because standard dataclasses are unhashable by default when mutable, and sqlatypemodel requires hashing to track parent relationships.
from dataclasses import dataclass, asdict
@dataclass
class Config(MutableMixin):
retries: int
host: str
# REQUIRED: Restore identity hashing for change tracking
__hash__ = object.__hash__
# Usage in SQLAlchemy
config_col: Mapped[Config] = mapped_column(
ModelType(
Config,
json_dumps=asdict,
json_loads=lambda d: Config(**d)
)
)
Attrs
If you use the attrs library, disable equality-based hashing (eq=False) or explicitly set hash logic to ensure the object is hashable by ID.
import attrs
@attrs.define(eq=False) # eq=False enables identity hashing automatically
class AttrsConfig(MutableMixin):
mode: str
# Usage
attrs_col: Mapped[AttrsConfig] = mapped_column(
ModelType(
AttrsConfig,
json_dumps=attrs.asdict,
json_loads=lambda d: AttrsConfig(**d)
)
)
Custom Classes
You can use any class. If it doesn't have model_dump/model_validate (like Pydantic), simply provide the serialization logic.
class MyBucket(MutableMixin):
def __init__(self, items):
self.items = items
def to_json(self):
return {"items": self.items}
# Usage
bucket_col: Mapped[MyBucket] = mapped_column(
ModelType(
MyBucket,
json_dumps=lambda x: x.to_json(),
json_loads=lambda d: MyBucket(d["items"])
)
)
Important Caveats
Identity Hashing
To support robust parent tracking (required for nested mutation detection), MutableMixin enforces identity-based hashing (object.__hash__) or requires you to enable it (for dataclasses).
- Implication: Two model instances with identical data will have different hashes.
- Restriction: Do not use these models as keys in
dictorsetif you rely on value equality for deduplication. Use lists or value-based comparison logic instead.
In-Place Mutations & Sessions
For in-place mutations (like user.data.list.append(1)) to trigger a database update, the object must be attached to an active SQLAlchemy session. This is standard SQLAlchemy behavior for mutable types.
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sqlatypemodel-0.4.0.tar.gz.
File metadata
- Download URL: sqlatypemodel-0.4.0.tar.gz
- Upload date:
- Size: 11.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.2.1 CPython/3.14.2 Linux/6.17.11-300.fc43.x86_64
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7adfcd0e762b60aa7370d338a7f180fe0182afdef7b0281ab96820782202d217
|
|
| MD5 |
9012eccd745b5ac65fa096e1af160446
|
|
| BLAKE2b-256 |
645ff4e9675e65c9179781fd7fb81a4200b4af32785f47b3d0c8eb6b96603659
|
File details
Details for the file sqlatypemodel-0.4.0-py3-none-any.whl.
File metadata
- Download URL: sqlatypemodel-0.4.0-py3-none-any.whl
- Upload date:
- Size: 11.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.2.1 CPython/3.14.2 Linux/6.17.11-300.fc43.x86_64
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9446cddd34ba5cd532fc52cee473f9b1fa0ca03da2288e04e69d6fff105633f8
|
|
| MD5 |
d7b33a25e2306a2787a0737fd4cd4a98
|
|
| BLAKE2b-256 |
eab653ecd6ed0a70f694d262cc887a9fba68aee945a277e8d74fbce3e6ffb41e
|