Skip to content

Commit d54ef32

Browse files
committed
feat: add fastsqla-models agent skill
1 parent 6e98cf4 commit d54ef32

1 file changed

Lines changed: 263 additions & 0 deletions

File tree

skills/fastsqla-models/SKILL.md

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
---
2+
name: fastsqla-models
3+
description: >
4+
How to define ORM models with FastSQLA. Covers fastsqla.Base
5+
(DeclarativeBase + DeferredReflection), fully declared models, reflected
6+
models, Pydantic response models, SQLModel integration, and
7+
extend_existing. Includes a comparison table of Base vs SQLModel tradeoffs.
8+
---
9+
10+
# FastSQLA Models
11+
12+
FastSQLA provides `Base`, a pre-configured SQLAlchemy declarative base that
13+
combines `DeclarativeBase` with `DeferredReflection`. Alternatively, you can
14+
use SQLModel for models that are both ORM and Pydantic.
15+
16+
## `fastsqla.Base`
17+
18+
```python
19+
from sqlalchemy.ext.asyncio import AsyncSession
20+
from sqlalchemy.ext.declarative import DeferredReflection
21+
from sqlalchemy.orm import DeclarativeBase
22+
23+
24+
class Base(DeclarativeBase, DeferredReflection):
25+
__abstract__ = True
26+
```
27+
28+
Because `Base` inherits from `DeferredReflection`, table metadata is **not**
29+
loaded at import time. Instead, `Base.prepare()` is called inside FastSQLA's
30+
lifespan during app startup, which reflects all tables from the database in a
31+
single pass.
32+
33+
This means model attributes (columns, relationships) are **not available**
34+
until the app lifespan has started.
35+
36+
## Fully Declared Models
37+
38+
Standard SQLAlchemy 2.0 declarative mapping. You define every column
39+
explicitly using `Mapped` and `mapped_column`:
40+
41+
```python
42+
from fastsqla import Base
43+
from sqlalchemy.orm import Mapped, mapped_column
44+
45+
46+
class Hero(Base):
47+
__tablename__ = "hero"
48+
49+
id: Mapped[int] = mapped_column(primary_key=True)
50+
name: Mapped[str] = mapped_column(unique=True)
51+
secret_identity: Mapped[str]
52+
age: Mapped[int]
53+
```
54+
55+
Columns are fully typed and available for IDE autocompletion even before
56+
`Base.prepare()` runs. The `DeferredReflection` base still applies — the
57+
actual table binding happens at startup.
58+
59+
### Relationships
60+
61+
Declare relationships the standard SQLAlchemy way:
62+
63+
```python
64+
from sqlalchemy import ForeignKey, String
65+
from sqlalchemy.orm import Mapped, mapped_column, relationship
66+
67+
from fastsqla import Base
68+
69+
70+
class Team(Base):
71+
__tablename__ = "team"
72+
73+
id: Mapped[int] = mapped_column(primary_key=True)
74+
name: Mapped[str] = mapped_column(String, unique=True)
75+
heroes: Mapped[list["Hero"]] = relationship(back_populates="team")
76+
77+
78+
class Hero(Base):
79+
__tablename__ = "hero"
80+
81+
id: Mapped[int] = mapped_column(primary_key=True)
82+
name: Mapped[str] = mapped_column(unique=True)
83+
team_id: Mapped[int] = mapped_column(ForeignKey("team.id"))
84+
team: Mapped["Team"] = relationship(back_populates="heroes")
85+
```
86+
87+
## Reflected Models (DeferredReflection)
88+
89+
For tables that already exist in the database, declare only the table name.
90+
All columns are auto-reflected from the database schema when `Base.prepare()`
91+
runs at startup:
92+
93+
```python
94+
from fastsqla import Base
95+
96+
97+
class User(Base):
98+
__tablename__ = "user"
99+
```
100+
101+
After the lifespan starts, `User` has all columns from the `user` table as
102+
attributes (e.g. `User.id`, `User.email`).
103+
104+
**Important:** Reflected model attributes are **not available** until
105+
`Base.prepare()` has been called. Do not access column attributes at module
106+
level or during import.
107+
108+
You can mix reflected and fully declared models freely — both inherit from the
109+
same `Base`.
110+
111+
## Pydantic Response Models
112+
113+
`fastsqla.Base` models are **not** Pydantic models. To use them as FastAPI
114+
response types, create a separate Pydantic `BaseModel` with
115+
`ConfigDict(from_attributes=True)`:
116+
117+
```python
118+
from fastsqla import Base
119+
from pydantic import BaseModel, ConfigDict
120+
from sqlalchemy.orm import Mapped, mapped_column
121+
122+
123+
class Hero(Base):
124+
__tablename__ = "hero"
125+
126+
id: Mapped[int] = mapped_column(primary_key=True)
127+
name: Mapped[str] = mapped_column(unique=True)
128+
secret_identity: Mapped[str]
129+
age: Mapped[int]
130+
131+
132+
class HeroResponse(BaseModel):
133+
model_config = ConfigDict(from_attributes=True)
134+
135+
id: int
136+
name: str
137+
secret_identity: str
138+
age: int
139+
```
140+
141+
Then use `HeroResponse` in your endpoint:
142+
143+
```python
144+
from fastapi import FastAPI
145+
from fastsqla import Item, Session
146+
from sqlalchemy import select
147+
148+
app = FastAPI()
149+
150+
151+
@app.get("/heroes/{hero_id}")
152+
async def get_hero(hero_id: int, session: Session) -> Item[HeroResponse]:
153+
hero = await session.get(Hero, hero_id)
154+
return {"data": hero}
155+
```
156+
157+
FastAPI automatically converts the ORM instance to `HeroResponse` via
158+
`from_attributes`.
159+
160+
## SQLModel Alternative
161+
162+
Install with the `sqlmodel` extra:
163+
164+
```bash
165+
pip install FastSQLA[sqlmodel]
166+
```
167+
168+
SQLModel models are both SQLAlchemy ORM models **and** Pydantic models. No
169+
separate response model is needed:
170+
171+
```python
172+
from sqlmodel import Field, SQLModel
173+
174+
175+
class Hero(SQLModel, table=True):
176+
id: int | None = Field(default=None, primary_key=True)
177+
name: str = Field(unique=True)
178+
secret_identity: str
179+
age: int
180+
```
181+
182+
Use directly in endpoints:
183+
184+
```python
185+
from fastapi import FastAPI
186+
from fastsqla import Item, Session
187+
from sqlmodel import select
188+
189+
app = FastAPI()
190+
191+
192+
@app.get("/heroes/{hero_id}")
193+
async def get_hero(hero_id: int, session: Session) -> Item[Hero]:
194+
hero = (await session.execute(select(Hero).where(Hero.id == hero_id))).scalar_one()
195+
return {"data": hero}
196+
```
197+
198+
### Session Swap
199+
200+
When SQLModel is installed, FastSQLA silently uses
201+
`sqlmodel.ext.asyncio.session.AsyncSession` instead of SQLAlchemy's
202+
`AsyncSession`. This happens automatically — no configuration needed:
203+
204+
```python
205+
# src/fastsqla.py lines 23-27
206+
try:
207+
from sqlmodel.ext.asyncio.session import AsyncSession
208+
except ImportError:
209+
pass
210+
```
211+
212+
The `Session` dependency and all internal session handling use whichever
213+
`AsyncSession` was imported.
214+
215+
### `extend_existing`
216+
217+
When using SQLModel, if another part of your code (or a library) has already
218+
registered a table with the same name in SQLAlchemy's metadata, add
219+
`extend_existing`:
220+
221+
```python
222+
from sqlmodel import Field, SQLModel
223+
224+
225+
class Hero(SQLModel, table=True):
226+
__table_args__ = {"extend_existing": True}
227+
228+
id: int | None = Field(default=None, primary_key=True)
229+
name: str = Field(unique=True)
230+
secret_identity: str
231+
age: int
232+
```
233+
234+
This tells SQLAlchemy to merge the model definition with the existing table
235+
metadata instead of raising an error.
236+
237+
### SQLModel Does Not Use `fastsqla.Base`
238+
239+
SQLModel models inherit from `SQLModel`, not from `fastsqla.Base`. They do
240+
**not** participate in `DeferredReflection` and do **not** support column
241+
reflection. All columns must be explicitly declared.
242+
243+
## Base vs SQLModel Comparison
244+
245+
| Feature | `fastsqla.Base` | SQLModel |
246+
|---|---|---|
247+
| Column reflection | Yes — declare only `__tablename__`, columns auto-reflected | No — all columns must be declared |
248+
| Separate Pydantic model needed | Yes — `BaseModel` with `from_attributes=True` | No — model is both ORM and Pydantic |
249+
| Validation on assignment | No | Yes — Pydantic validation on fields |
250+
| Dependencies | SQLAlchemy only | SQLAlchemy + SQLModel + Pydantic |
251+
| Relationships | Standard SQLAlchemy `relationship()` | SQLModel `Relationship()` (wraps SQLAlchemy) |
252+
| DeferredReflection | Yes — `Base.prepare()` at startup | No — tables registered at import time |
253+
| Install | `pip install FastSQLA` | `pip install FastSQLA[sqlmodel]` |
254+
255+
## Quick Reference
256+
257+
| What you need | What to use |
258+
|---|---|
259+
| Fully declared model with explicit columns | `class MyModel(Base)` with `Mapped` + `mapped_column` |
260+
| Reflected model from existing table | `class MyModel(Base)` with only `__tablename__` |
261+
| Response model for Base | Separate `BaseModel` with `ConfigDict(from_attributes=True)` |
262+
| Combined ORM + Pydantic model | `class MyModel(SQLModel, table=True)` |
263+
| Table already in metadata (SQLModel) | Add `__table_args__ = {"extend_existing": True}` |

0 commit comments

Comments
 (0)