|
| 1 | +--- |
| 2 | +name: sqlmodel |
| 3 | +description: Use when writing or reviewing Python code with SQLModel, especially models, sessions, queries, FastAPI integration, relationships, link models, creates, updates, and deletes. |
| 4 | +--- |
| 5 | + |
| 6 | +# SQLModel Patterns |
| 7 | + |
| 8 | +Use SQLModel's API first. Do not default to raw SQLAlchemy patterns unless the task explicitly needs a SQLAlchemy-only feature. |
| 9 | + |
| 10 | +## Imports |
| 11 | + |
| 12 | +Prefer imports from `sqlmodel`: |
| 13 | + |
| 14 | +```python |
| 15 | +from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select |
| 16 | +``` |
| 17 | + |
| 18 | +Do not use SQLAlchemy declarative defaults such as `declarative_base()`, `Mapped[...]`, `mapped_column()`, `relationship()`, or `sessionmaker()` for normal SQLModel code. |
| 19 | + |
| 20 | +## Models |
| 21 | + |
| 22 | +Define table models with `SQLModel, table=True` and `Field()`: |
| 23 | + |
| 24 | +```python |
| 25 | +class Hero(SQLModel, table=True): |
| 26 | + id: int | None = Field(default=None, primary_key=True) |
| 27 | + name: str = Field(index=True) |
| 28 | + team_id: int | None = Field(default=None, foreign_key="team.id") |
| 29 | +``` |
| 30 | + |
| 31 | +Use `Field(default_factory=...)` for generated values: |
| 32 | + |
| 33 | +```python |
| 34 | +id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) |
| 35 | +``` |
| 36 | + |
| 37 | +Use non-table `SQLModel` classes for create/update/public API schemas instead of mixing request/response-only fields into table models. |
| 38 | + |
| 39 | +## Sessions and Queries |
| 40 | + |
| 41 | +Open sessions directly with the engine: |
| 42 | + |
| 43 | +```python |
| 44 | +with Session(engine) as session: |
| 45 | + heroes = session.exec(select(Hero)).all() |
| 46 | +``` |
| 47 | + |
| 48 | +Do not create a `sessionmaker()` for typical SQLModel examples. |
| 49 | + |
| 50 | +Use `session.exec(select(...))`, not `session.execute(...)` and not `session.query(...)`. SQLModel's `exec()` handles scalar results so agents should not add `.scalars()` after selects of models. |
| 51 | + |
| 52 | +Use `session.get(Model, id)` for primary-key lookups. |
| 53 | + |
| 54 | +Use normal result methods after `session.exec(...)`: `.all()` for lists, `.first()` for optional first rows, `.one()` when exactly one row must exist, and `.one_or_none()` when zero or one row is valid. |
| 55 | + |
| 56 | +After creating or mutating objects, commit and refresh when returned data needs DB defaults or generated IDs: |
| 57 | + |
| 58 | +```python |
| 59 | +session.add(hero) |
| 60 | +session.commit() |
| 61 | +session.refresh(hero) |
| 62 | +``` |
| 63 | + |
| 64 | +## Relationships |
| 65 | + |
| 66 | +Use SQLModel relationship attributes, not ad-hoc SQLAlchemy tables or `secondary=`. |
| 67 | + |
| 68 | +One-to-many: |
| 69 | + |
| 70 | +```python |
| 71 | +class Team(SQLModel, table=True): |
| 72 | + id: int | None = Field(default=None, primary_key=True) |
| 73 | + heroes: list["Hero"] = Relationship(back_populates="team") |
| 74 | + |
| 75 | + |
| 76 | +class Hero(SQLModel, table=True): |
| 77 | + id: int | None = Field(default=None, primary_key=True) |
| 78 | + team_id: int | None = Field(default=None, foreign_key="team.id") |
| 79 | + team: Team | None = Relationship(back_populates="heroes") |
| 80 | +``` |
| 81 | + |
| 82 | +Many-to-many: |
| 83 | + |
| 84 | +```python |
| 85 | +class HeroTeamLink(SQLModel, table=True): |
| 86 | + team_id: int | None = Field(default=None, foreign_key="team.id", primary_key=True) |
| 87 | + hero_id: int | None = Field(default=None, foreign_key="hero.id", primary_key=True) |
| 88 | + |
| 89 | + |
| 90 | +class Team(SQLModel, table=True): |
| 91 | + id: int | None = Field(default=None, primary_key=True) |
| 92 | + heroes: list["Hero"] = Relationship(back_populates="teams", link_model=HeroTeamLink) |
| 93 | + |
| 94 | + |
| 95 | +class Hero(SQLModel, table=True): |
| 96 | + id: int | None = Field(default=None, primary_key=True) |
| 97 | + teams: list[Team] = Relationship(back_populates="heroes", link_model=HeroTeamLink) |
| 98 | +``` |
| 99 | + |
| 100 | +If the link table has extra fields, model it as a full SQLModel table with relationships to both sides, and interact with the link objects directly. |
| 101 | + |
| 102 | +When starting a new app, prefer keeping related SQLModel table models in one file to simplify relationship annotations and metadata ordering. If models must be split across files, use `TYPE_CHECKING` imports plus string annotations: |
| 103 | + |
| 104 | +```python |
| 105 | +if TYPE_CHECKING: |
| 106 | + from .team_model import Team |
| 107 | + |
| 108 | +team: Optional["Team"] = Relationship(back_populates="heroes") |
| 109 | +``` |
| 110 | + |
| 111 | +For SQLAlchemy relationship options not exposed as first-class SQLModel parameters, prefer `Relationship(sa_relationship_kwargs={...})` or `Relationship(sa_relationship_args=[...])`. Use `sa_relationship=relationship(...)` only as an escape hatch when SQLModel's relationship wrapper cannot express the mapping. |
| 112 | + |
| 113 | +## Creates and Updates |
| 114 | + |
| 115 | +Use direct construction for trusted internal values: |
| 116 | + |
| 117 | +```python |
| 118 | +hero = Hero(name="Deadpond", secret_name="Dive Wilson") |
| 119 | +``` |
| 120 | + |
| 121 | +Build table objects from data models with `Model.model_validate(...)`: |
| 122 | + |
| 123 | +```python |
| 124 | +db_hero = Hero.model_validate(hero_create) |
| 125 | +``` |
| 126 | + |
| 127 | +For FastAPI, use a split model pattern: `HeroBase` for shared fields, `Hero(HeroBase, table=True)` for the table, `HeroCreate` for input, `HeroUpdate` with all-optional fields for PATCH, `HeroPublic` for output, and relationship-specific public models such as `HeroPublicWithTeam` only where needed. |
| 128 | + |
| 129 | +For partial updates, dump only provided fields and update in place: |
| 130 | + |
| 131 | +```python |
| 132 | +hero_data = hero_update.model_dump(exclude_unset=True) |
| 133 | +db_hero.sqlmodel_update(hero_data) |
| 134 | +session.add(db_hero) |
| 135 | +session.commit() |
| 136 | +session.refresh(db_hero) |
| 137 | +``` |
| 138 | + |
| 139 | +## Metadata and App Setup |
| 140 | + |
| 141 | +Call `SQLModel.metadata.create_all(engine)` only after all table model classes have been imported. In FastAPI examples, use a dependency that yields `Session(engine)`. |
| 142 | + |
| 143 | +For tests and small examples, SQLite is a common option; when using it, create metadata explicitly and use direct sessions: |
| 144 | + |
| 145 | +```python |
| 146 | +engine = create_engine("sqlite:///:memory:") |
| 147 | +SQLModel.metadata.create_all(engine) |
| 148 | +with Session(engine) as session: |
| 149 | + ... |
| 150 | +``` |
| 151 | + |
| 152 | +When using SQLite with FastAPI, include: |
| 153 | + |
| 154 | +```python |
| 155 | +connect_args = {"check_same_thread": False} |
| 156 | +engine = create_engine(sqlite_url, connect_args=connect_args) |
| 157 | +``` |
| 158 | + |
| 159 | +## Deletes |
| 160 | + |
| 161 | +Use SQLModel's relationship helpers for cascades: |
| 162 | + |
| 163 | +```python |
| 164 | +heroes: list["Hero"] = Relationship(back_populates="team", cascade_delete=True) |
| 165 | +team_id: int | None = Field(default=None, foreign_key="team.id", ondelete="CASCADE") |
| 166 | +``` |
| 167 | + |
| 168 | +For database-level delete behavior such as `SET NULL`, pair `ondelete` with a nullable foreign key and use `passive_deletes` on the relationship when appropriate. |
| 169 | + |
| 170 | +`ondelete="SET NULL"` requires a nullable foreign key: |
| 171 | + |
| 172 | +```python |
| 173 | +team_id: int | None = Field(default=None, foreign_key="team.id", ondelete="SET NULL") |
| 174 | +``` |
| 175 | + |
| 176 | +## SQLAlchemy Escape Hatches |
| 177 | + |
| 178 | +Use SQLModel's `Field(sa_type=...)`, `Field(sa_column=...)`, `Field(sa_column_args=...)`, or `Field(sa_column_kwargs=...)` only when normal `Field()` parameters do not cover a column or type requirement. Do not switch the whole model to SQLAlchemy declarative style for one custom column. |
0 commit comments