Skip to main content

Typed JSON fields for SQLAlchemy with automatic mutation tracking

Project description

sqlatypemodel

Tests PyPI version Python versions

Typed JSON fields for SQLAlchemy with automatic mutation tracking.

SQLAlchemy typically requires you to replace the entire JSON object to trigger an update. sqlatypemodel changes that. It allows you to work with strictly typed Python objects (Pydantic, Dataclasses, Attrs, or custom classes) while ensuring every change—no matter how deep—is automatically saved to the database.

It is powered by orjson, offering blazing-fast performance and native support for datetime, UUID, and numpy.

Now with full support for Pickle, making it perfect for Celery tasks and caching.

✨ Key Features

  • Seamless Integration: Store Pydantic models directly in SQLAlchemy columns.
  • Universal Support: Works with Pydantic (V1 & V2), Dataclasses, Attrs, and custom classes.
  • Pickle & Cache Ready: Objects can be pickled, sent to Celery workers, or cached in Redis without losing tracking capabilities.
  • Mutation Tracking: Built-in MutableMixin detects deep changes (e.g., user.data.list.append("item")) and flags the row for update.
  • High Performance:
    • Powered by orjson: Rust-based serialization is 10x-50x faster than standard json.
    • O(1) Wrapping: Smart "short-circuit" logic prevents re-wrapping already tracked collections.
    • Atomic Optimization: Skips overhead for atomic types (int, str, bool).

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 automatically:

# ✅ Persisted automatically
user.settings.theme = "dark"
user.tags.append("new")
session.commit() # UPDATE "users" SET ...

Installation

pip install sqlatypemodel

Or

pip install sqlatypemodel[fast]

To install with orjson

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, Field
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
from sqlatypemodel import ModelType, MutableMixin
from sqlatypemodel.sqlalchemy_utils import create_sync_engine

# 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] = Field(default_factory=list)

# 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
# Use our helper to get an orjson-optimized engine
engine = create_sync_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

Celery & Caching Support (Pickle)

One of the hardest challenges with SQLAlchemy models is passing them to background tasks (like Celery) or caching them. Standard mutable tracking often breaks during serialization.

sqlatypemodel solves this. You can safely pickle your models:

import pickle

# 1. User is loaded from DB
user = session.get(User, 1)

# 2. Serialize and send to a worker (e.g. RabbitMQ/Redis)
payload = pickle.dumps(user)

# --- In Worker Process ---

# 3. Deserialize
worker_user = pickle.loads(payload)

# 4. Modify
worker_user.settings.theme = "worker_updated"

# 5. Send back or Merge
session.merge(worker_user)
session.commit() # Updates are saved!

Handling Lists and Dicts

⚠️ Important: ModelType expects a structured model. Do not pass raw List[int] or Dict directly to ModelType. Wrap them in a model.

Incorrect:

# ❌ Will raise ValueError
col: Mapped[List[int]] = mapped_column(ModelType(List[int]))

Correct:

class ListWrapper(MutableMixin, BaseModel):
    items: List[int] = Field(default_factory=list)

class MyEntity(Base):
    # ...
    # ✅ Works perfectly
    raw_list: Mapped[ListWrapper] = mapped_column(
        ModelType(ListWrapper), 
        default_factory=ListWrapper
    )

🔧 Under the Hood: Architecture

1. orjson Power

We use orjson for serialization. It is ~50x faster than json and supports types that normally break standard serializers: datetime, UUID, numpy arrays, and dataclasses.

2. Utilities: Easy Engine Configuration

To use the full power of sqlatypemodel, your SQLAlchemy Engine must be configured to use orjson. We provide helpers to do this automatically:

from sqlatypemodel.sqlalchemy_utils import create_sync_engine, create_async_engine

# Sync (SQLite, Postgres, etc.)
engine = create_sync_engine("postgresql://user:pass@localhost/db")

# Async (asyncpg, aiosqlite)
engine = await create_async_engine("postgresql+asyncpg://...")

Advanced Usage

sqlatypemodel is not limited to Pydantic.

Python Dataclasses

We automatically patch __hash__ for dataclasses to ensure tracking works, so you can use them out of the box.

from dataclasses import dataclass, asdict

@dataclass
class Config(MutableMixin):
    retries: int
    host: str

# Usage
config_col: Mapped[Config] = mapped_column(
    ModelType(
        Config,
        json_dumps=asdict,
        json_loads=lambda d: Config(**d)
    )
)

⚠️ Important Caveats

64-bit Integer Limit

Since orjson is written in Rust, it strictly adheres to 64-bit signed integer limits (from -2^63 to 2^63 - 1). If you try to save a larger integer, a SerializationError will be raised. This is a trade-off for performance.

Identity Hashing

To track changes, MutableMixin uses identity-based hashing (object.__hash__). This means two models with the same data are considered "different" keys in a dictionary. Avoid using these mutable models as keys in sets or dicts if you rely on value equality.

Verification & Stress Testing

Reliability is paramount. We include a forensic-grade stress test suite that anyone can run.

The suite performs:

  1. Hypothesis (Property-based testing): Generates thousands of edge cases (deep nesting, Unicode, large numbers).
  2. Concurrency Test: Verifies safety in multi-threaded environments.
  3. Rollback Integrity: Guarantees state consistency after transaction rollbacks.
poetry run pytest tests/test_stress.py

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

sqlatypemodel-0.6.0.tar.gz (17.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sqlatypemodel-0.6.0-py3-none-any.whl (17.3 kB view details)

Uploaded Python 3

File details

Details for the file sqlatypemodel-0.6.0.tar.gz.

File metadata

  • Download URL: sqlatypemodel-0.6.0.tar.gz
  • Upload date:
  • Size: 17.8 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

Hashes for sqlatypemodel-0.6.0.tar.gz
Algorithm Hash digest
SHA256 665b53a6a91eb0c54b7d8981e6a2e8a47ca25a6325cb1fb958a654fa8408ae60
MD5 fb7b147937e9382f737124471e4caae3
BLAKE2b-256 321f61a0c2989a8eefdf2e9a8efe51d1913f0cf4e8d24c32c9d45f54c161ddc6

See more details on using hashes here.

File details

Details for the file sqlatypemodel-0.6.0-py3-none-any.whl.

File metadata

  • Download URL: sqlatypemodel-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 17.3 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

Hashes for sqlatypemodel-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b39cafcd5dad9ccbd7a14e32312310ee69ee0baba7a1b1c894537387bf6f74db
MD5 1ef727d89e13255d531a35a5ba6806f2
BLAKE2b-256 33d0bb2bd9f2c3257ca86bc744f7bc857ba4a40b95b76cbcd95680cdd490ffb6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page