Skip to content

Commit 8c47440

Browse files
authored
✨ Add SQLModel Agent Library Skill, install with uvx library-skills (#2017)
1 parent 558ab39 commit 8c47440

1 file changed

Lines changed: 178 additions & 0 deletions

File tree

  • sqlmodel/.agents/skills/sqlmodel
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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

Comments
 (0)