SQLAlchemy Integration
Granite Storage ships with first-class SQLAlchemy support: a column type that
transparently serialises StoredObjectRef to/from JSON, a mixin that adds
file-handling methods directly to any ORM model, and an Alembic helper for
portable column definitions.
StoredObjectRefType — Column Type
Use StoredObjectRefType as the column type for any column that stores a
file reference. It wraps SQLAlchemy’s JSON (PostgreSQL: JSONB) and
automatically converts between the column value and a StoredObjectRef
instance.
from sqlalchemy import String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from granite_storage import StoredObjectRef, StoredObjectRefType
class Base(DeclarativeBase):
pass
class UserProfile(Base):
__tablename__ = "user_profile"
id: Mapped[str] = mapped_column(String, primary_key=True)
name: Mapped[str] = mapped_column(String)
avatar: Mapped[StoredObjectRef | None] = mapped_column(
StoredObjectRefType, nullable=True
)
When you read the row back from the database avatar is already a
StoredObjectRef (or None). No manual from_dict / to_dict needed.
StoredContentMixin — File-Aware Models
StoredContentMixin adds set_content(), set_content_stream(),
get_content(), and delete_content() helpers directly to your model class.
You only need to declare two class variables:
__stored_content_field_name__— the attribute that holds theStoredObjectRef.__stored_content_storage_key__— the policy name to use by default.
Then configure the manager at startup and inject it into the class.
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from granite_storage import (
StorageManager, StoragePolicy, StoredContentMixin,
StoredObjectRef, StoredObjectRefType,
)
from granite_storage.backends.local import LocalStorageBackend
class Base(DeclarativeBase):
pass
class Document(Base, StoredContentMixin):
__tablename__ = "document"
__stored_content_field_name__ = "_file_ref"
__stored_content_storage_key__ = "documents"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column()
_file_ref: Mapped[StoredObjectRef | None] = mapped_column(
StoredObjectRefType, nullable=True
)
# Inject the manager once at startup
manager = StorageManager(
backends={"local": LocalStorageBackend("/var/uploads")},
policies={
"documents": StoragePolicy("documents", "local", max_size=10*1024*1024),
},
)
Document.__storage_manager__ = manager
# Now any instance can store content
doc = Document(id=1, title="Report")
session.add(doc)
session.flush() # ensure id is set
doc.set_content(pdf_bytes, filename="report.pdf", content_type="application/pdf")
session.commit()
# Replace existing content
result = doc.replace_content(new_pdf_bytes, filename="report-v2.pdf")
# result.previous_ref → the old StoredObjectRef (for cleanup)
# result.new_ref → the new StoredObjectRef
# Read back
data = doc.get_content() # bytes
# Delete stored file (sets _file_ref to None)
doc.delete_content()
session.commit()
Alembic — Portable Column Type
When writing Alembic migrations use portable_storage_ref_type() so that the
column is JSON on SQLite and JSONB on PostgreSQL without duplication:
import sqlalchemy as sa
from alembic import op
from granite_storage import portable_storage_ref_type
def upgrade():
op.add_column(
"document",
sa.Column("_file_ref", portable_storage_ref_type(), nullable=True),
)
def downgrade():
op.drop_column("document", "_file_ref")
Migrating an Existing Text Column
If you have a column that previously stored inline text and want to migrate it
to a StoredObjectRef JSON column on PostgreSQL:
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
def upgrade():
op.alter_column(
"section_item",
"content_markdown",
existing_type=sa.Text(),
type_=sa.JSON().with_variant(postgresql.JSONB(), "postgresql"),
postgresql_using=(
"CASE WHEN content_markdown IS NULL THEN NULL "
"ELSE jsonb_build_object('legacy_inline_text', content_markdown) END"
),
existing_nullable=True,
)