010. SQL Models defined in SQLAlchemy/Alembic

Status: proposed

Authors: Josh Holland

Date: 2025-06-09

Context and Problem Statement

As we build more components making use of a PostgreSQL database, where can we store schema definitions so that they are reusable across projects? What’s the best client library to query the database?

Decision Drivers

Considered Options

Decision Outcome

Chosen option: SQLAlchemy declarative models with Alembic in a centralised repo because it is a relatively light-weight solution that still provides many features. There do not appear to be any realistic alternatives to Alembic for managing migrations.

Positive Consequences

Negative Consequences

Pros and Cons of the Options

SQLAlchemy declarative models in central repo

Define the models in a single repo, then import them into client applications.

# central repo
from datetime import datetime

from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

class Base(DeclarativeBase):
    pass

class PhenocamTag(Base):
    __tablename__ = "phenocam_tags"

    id: Mapped[int] = mapped_column(primary_key=True)
    last_updated: Mapped[datetime]
    image: Mapped[str]
    site_id: Mapped[str]
    activity: Mapped[str]
    new_land_cover: Mapped[str]

# consuming application
from sqlalchemy import select

from dri_database_models import PhenocamTag

def foo(site_id):
    with Session(engine) as session:
        query = select(PhenocamTag).where(PhenocamTag.site_id == site_id).order_by(PhenocamTag.last_updated)
        tags = session.execute(query).all()

SQLAlchemy declarative models in individual repos

Each subcomponent which interfaces with the database defines its own declarative models.

SQLAlchemy core with custom marshalling

Models are defined independently of SQLAlchemy and translated into each component’s classes.

# consuming application
from datetime import datetime

from sqlalchemy import select, Table, Column, String, DateTime, MetaData

metadata_obj = MetaData()

class PhenocamTag:
    last_updated: datetime
    image: str
    site_id: str
    activity: str
    new_land_cover: str

def foo(site_id):
    tag_table = Table(
        "phenocam_tags",
        metadata_obj,
        Column("last_updated", DateTime),
        Column("image", String),
        # etc
    )
    with engine.connect() as conn:
        query = select(tag_table).where(tag_table.site_id == site_id).order_by(tag_table.last_updated)
        tags = [PhenocamTag(...) for ... insession.execute(query).all()]

SQLModel

Use Tiangelo’s SQLModel library which wraps SQLAlchemy and integrates it with Pydantic.