🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)
🕵️ SicherheitslückenHak5: Hackers Just Poisoned the Rust Supply Chain | Threat Wire(01.09.2026 um 14:00 Uhr)
🕵️ SicherheitslückenHak5: Hackers Found a Way Into Humanoid Robots | Threat Wire(04.09.2026 um 15:04 Uhr)
🔧 AI Nachrichten Bits und so #1021 (Passwort für Laufwerk)(31.08.2026 um 22:15 Uhr)
🔧 AI Nachrichten Bits und so #1022 (Wie Weißbier)(06.09.2026 um 20:39 Uhr)
🍏 iOS / Mac OSHue-App 6.0 ist da: das sind die Neuerungen(07.09.2026 um 17:21 Uhr)

🔧 Programmierung 🕛 kürzlich 12 Min Lesezeit
0

ORM tipado con migraciones automáticas: Fitz vs SQLAlchemy + Alembic + Pydantic

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht

Para tener un ORM tipado con migraciones automáticas en Python necesitás mantener 3 fuentes de verdad (SQLAlchemy + Pydantic + Alembic). En Fitz es UN type con decoradores. Plus 8× RPS y 5× menos memoria que SQLAlchemy en bench reproducible.







El doble (triple) tipado del stack Python



Para cada entidad de tu DB en una API FastAPI moderna tenés que mantener:





  • models.py — modelo SQLAlchemy (define el schema).


  • schemas.py — modelos Pydantic (define request/response).


  • alembic/versions/*.py — migraciones autogeneradas.




CODE
# models.py
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.orm import relationship, declarative_base
from datetime import datetime

Base = declarative_base()

class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
email = Column(String, nullable=False, unique=True)
name = Column(String, nullable=False)
role = Column(String, nullable=False, default="user")
created_at = Column(DateTime, default=datetime.utcnow)
posts = relationship("Post", back_populates="user")

class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
title = Column(String, nullable=False)
body = Column(String, nullable=False)
user = relationship("User", back_populates="posts")









CODE
# schemas.py
from pydantic import BaseModel
from datetime import datetime

class UserCreate(BaseModel):
email: str
name: str
role: str = "user"

class UserOut(BaseModel):
id: int
email: str
name: str
role: str
created_at: datetime

class Config:
from_attributes = True

class PostCreate(BaseModel):
title: str
body: str

class PostOut(BaseModel):
id: int
user_id: int
title: str
body: str

class Config:
from_attributes = True

class UserWithPosts(UserOut):
posts: list[PostOut] = []









CODE
# main.py
@app.post("/users", response_model=UserOut)
async def create_user(user: UserCreate, db: AsyncSession = Depends(get_session)):
new_user = User(**user.model_dump())
db.add(new_user)
await db.commit()
await db.refresh(new_user)
return new_user

@app.get("/users/{id}", response_model=UserWithPosts)
async def get_user(id: int, db: AsyncSession = Depends(get_session)):
result = await db.execute(
select(User).options(selectinload(User.posts)).where(User.id == id)
)
user = result.scalar_one_or_none()
if not user:
raise HTTPException(404, "user not found")
return user






Tres archivos. Dos modelos de la misma entidad mantenidos en paralelo. Y para el schema:




CODE
$ alembic revision --autogenerate -m "add user role"
# Revisás el archivo generado — autogenerate a veces se confunde
$ alembic upgrade head






Plus configurar Alembic (env.py, alembic.ini, target_metadata...).






Lo mismo en Fitz






CODE
@table("users")
type User {
@primary id: Int,
email: Str,
name: Str,
role: Str = "user",
created_at: Str = "now()",
@has_many("Post", "user_id") posts: List<Post>,
}

@table("posts")
type Post {
@primary id: Int,
user_id: Int,
title: Str,
body: Str,
@belongs_to user: User?,
}

@post("/users")
async fn create_user(db: DbConn, user: User) -> Result<User> {
return User.insert(db, user).await
}

@get("/users/{id}")
async fn get_user(db: DbConn, id: Int) -> Result<User> {
return User.where(fn(u) => u.id == id).preload("posts").first(db).await
}






UN type. UNA fuente de verdad. El compilador genera:




  • El SQL parametrizado para las queries.

  • El schema OpenAPI desde el mismo type.

  • El deserializer JSON del body desde el mismo type.

  • La validación de fields requeridos / nullables / defaults.



Y el schema de la DB:




CODE
$ fitz db diff
+ CREATE TABLE users (
+ id BIGSERIAL PRIMARY KEY,
+ email TEXT NOT NULL,
+ name TEXT NOT NULL,
+ role TEXT NOT NULL DEFAULT 'user',
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
+ );
+ CREATE TABLE posts (
+ id BIGSERIAL PRIMARY KEY,
+ user_id BIGINT NOT NULL REFERENCES users(id),
+ title TEXT NOT NULL,
+ body TEXT NOT NULL
+ );

$ fitz db migrate
✓ aplicada migration 20260616_120300_initial.sql









La tabla cruda









































































Item Python (SQLAlchemy + Alembic + Pydantic) Fitz
Fuentes de verdad para una entidad 3 (models.py, schemas.py, alembic/) 1 (@table type)
Generar schema alembic revision --autogenerate -m "..." fitz db diff
Aplicar alembic upgrade head fitz db migrate
Rollback alembic downgrade -1 fitz db rollback
Query DSL
.filter(User.role == "admin") (DSL ORM)

.where(fn(u) => u.role == "admin") (closure-to-SQL en compile-time)
Eager loading .options(selectinload(User.posts)) .preload("posts")
Operadores Postgres
User.email.ilike(...), .contains(...), etc.
Same —u.email.ilike(...), u.tags.has(...)
Transactions
with session.begin(): + acordate del flush
db.transaction(fn(tx) async { ... }).await
Validación request body Pydantic separado Mismo type
Schema OpenAPI Pydantic infer Auto desde @table type
Driver libpq vía psycopg2/asyncpg
Puro Rust (sin libpq, sin tokio-postgres)
Performance vs SQLAlchemy baseline 8× RPS, 5× memoria menos (bench abajo)





Por partes






Closure-to-SQL en compile-time



Esto es lo que más me costó implementar y de lo que más orgulloso estoy. SQLAlchemy traduce su DSL a SQL en runtime — cada .filter(User.role == "admin") arma el AST en runtime, mide overhead, allocea, y emite SQL.



Fitz lo hace en compile-time:




CODE
let admins = User.where(fn(u) => u.role == "admin")
.order_by(fn(u) => u.created_at, "desc")
.limit(10)
.all(db).await?






El compilador analiza el closure fn(u) => u.role == "admin" y emite, una sola vez al compilar el binario, el string SQL constante:




CODE
SELECT id, email, name, role, created_at FROM users
WHERE role = $1 ORDER BY created_at DESC LIMIT 10






Más el código que bindea $1 = "admin". Cero overhead runtime para construir SQL — el string ya existe en el binario como &'static str.



Comparable en performance con Diesel o sqlx (que también generan SQL en compile-time vía macros), pero con sintaxis natural del lenguaje en lugar de macros procedurales.






Operadores Postgres nativos



SQLAlchemy:




CODE
admins = session.query(User).filter(User.email.ilike("%@example.com")).all()
tagged = session.query(User).filter(User.tags.contains(["admin"])).all()
pro_users = session.query(User).filter(User.metadata["plan"].astext == "pro").all()






Fitz (los operadores son métodos sobre el field):




CODE
let admins = User.where(fn(u) => u.email.ilike("%@example.com")).all(db).await?
let tagged = User.where(fn(u) => u.tags.has("admin")).all(db).await?
let pro_users = User.where(fn(u) => u.metadata.get("plan") == "pro").all(db).await?






Operadores cubiertos:




























Categoría Métodos
String
.like(p), .ilike(p), .starts_with(p), .ends_with(p), .contains(p) (con escape auto de %/_)
Listas/IN
.is_in([...]), .is_null(), .is_not_null()
Arrays
.has(x) (?), .contains_all(xs) (@>), .contained_in(xs) (<@)
JSONB
.has_key(k) (?), .has_all_keys(ks) (?&), .has_any_keys(ks) (`?\





Relations + eager loading



SQLAlchemy:

{% raw %}



CODE
class User(Base):
posts = relationship("Post", back_populates="user", lazy="select")

class Post(Base):
user = relationship("User", back_populates="posts")

# Eager loading
users = await db.execute(
select(User).options(selectinload(User.posts))
)





Plus configurar lazy= correctamente para no caer en el N+1 más viejo del mundo (queries lazy disparando una por user).



Fitz:




CODE
@table("users")
type User {
@primary id: Int,
name: Str,
@has_many("Post", "user_id") posts: List<Post>,
}

@table("posts")
type Post {
@primary id: Int,
user_id: Int,
@belongs_to user: User?,
}

// Eager loading explícito (la default es NO eager — sin sorpresas)
let users = User.preload("posts").all(db).await?






.preload("posts") emite una sola query batched con WHERE user_id IN (...) y arma los structs Fitz. La default es no eager: si no preload, el field virtual posts queda vacío y vos sabés que no fuiste a la DB.



Typo en el nombre del field:




CODE
let users = User.preload("postss").all(db).await?
// ^^^^^^^ error de compilación: User no tiene relation "postss"






El compilador chequea el match estáticamente. SQLAlchemy te lo dice en runtime cuando el request llega.






Transactions



SQLAlchemy:




CODE
async with db.begin():
user = User(...)
db.add(user)
await db.flush() # acordate del flush sino user.id está vacío
post = Post(user_id=user.id, ...)
db.add(post)






Fitz:




CODE
let result = db.transaction(fn(tx) async {
let user = User.insert(tx, User { id: 0, name: "Ada", role: "user" }).await?
let post = Post.insert(tx, Post { id: 0, user_id: user.id, title: "hola", body: "..." }).await?
return Ok(post)
}).await






El bloque adentro de db.transaction(...) recibe un tx: DbConn que escribe contra la transaction. Si el closure retorna Err, rollback; si retorna Ok, commit. Si la closure paniquea, rollback automático.



Y la User.insert(...) te devuelve el row con el id BIGSERIAL ya asignado, sin "acordate del flush".






Migraciones declarativas



Cambio en el type:




CODE
 @table("users")
type User {
@primary id: Int,
email: Str,
name: Str,
role: Str = "user",
+ avatar_url: Str?,
created_at: Str = "now()",
}






Diff:




CODE
$ fitz db diff
+ ALTER TABLE users ADD COLUMN avatar_url TEXT;






Aplicar:




CODE
$ fitz db migrate
✓ aplicada migration 20260616_153020_add_user_avatar_url.sql






Rollback:




CODE
$ fitz db rollback
✓ revertida migration 20260616_153020_add_user_avatar_url.sql






El compilador compara los @table types del código con el schema vivo de la DB y emite las diffs idempotentes. La migración se commitea (es un archivo .sql) para review en el PR. La fase OPS sigue siendo tu responsabilidad (timing del deploy, downtime planning para ALTER COLUMN type que reescribe la tabla, etc.) — pero el SQL ya está generado y revisado.



En Alembic, --autogenerate a veces genera SQL incorrecto (especialmente para constraints, índices compuestos, defaults complejos) y siempre se recomienda revisar el archivo manualmente. Fitz emite el diff sin tocar el SQL del usuario — vos lo ves y lo applicás.






¿Cuán rápido es? — bench reproducible



Promesas vacías son fáciles. El repo trae — Fitz + ORM nativo.


  • .



    Por qué Fitz es rápido: SQL constante en compile-time (cero overhead runtime de construir queries), driver Postgres puro Rust (sin libpq overhead), serialización JSON con tipos concretos (sin reflection), Arc<Mutex<>> para shared state HTTP (multi-thread sin GIL).



    Por qué SQLAlchemy es lento en este test: identity map, lazy loading flags, eventos, descriptors de columnas — muchas features que valen su costo cuando las necesitás, no cuando no.






    Lo que Fitz NO te da (todavía)



    Honestidad sobre las deudas:





    • Solo Postgres. No MySQL, no SQLite. Postgres es la elección por el shape del ecosystem (binary protocol claro, tipos ricos, JSONB, arrays). MySQL/SQLite quedan para cuando aparezca demanda.


    • BelongsTo en .preload(...) — sólo HasMany/HasOne en eager loading por ahora. BelongsTo eager: deuda residual.


    • Composite PKs@primary solo sobre un field. Tables con PK compuesta (raro en proyectos nuevos): no soportadas.


    • JSON operators de Postgres en .where — los más usados están (.has_key, .contains_json, .get), pero jsonb_path_query y similares solo via db.query(...) raw.


    • Read replicas / sharding. La conn pool es contra una sola DB. Para read replicas hay que coordinar a mano.


    • Migraciones con cero downtime para columns con NOT NULL sin default. El diff lo emite pero la fase OPS sigue siendo manual.


    • Performance tooling (EXPLAIN ANALYZE integrado en LSP) — no en MVP.






    Escape hatch: SQL crudo cuando lo necesites



    Para CTEs complejas, window functions, queries que el ORM no cubre:




    CODE
    let result = db.query("
    WITH ranked AS (
    SELECT id, name, ROW_NUMBER() OVER (PARTITION BY role ORDER BY created_at DESC) AS rn
    FROM users
    )
    SELECT * FROM ranked WHERE rn <= 3
    ", []).await?






    db.query(sql, params) devuelve List<Map<Str, Any>>. Sin chequeo estático del SQL ni de los column names — vos sabés. Para 90% del CRUD usás el ORM tipado; para queries chiflados, escape hatch.






    Cierre



    La queja más legítima contra los ORMs es "agregan una capa de abstracción que después tenés que romper para hacer queries serias". SQLAlchemy lo resuelve con un DSL muy expresivo, a costo de runtime overhead y curva de aprendizaje. Diesel/sqlx lo resuelven generando SQL en compile-time, a costo de macros procedurales y verbosidad.



    Fitz toma la ruta de Diesel/sqlx pero con sintaxis del lenguaje (no macros), un type checker que valida los closure-to-SQL, y migraciones del schema integradas como sub-comando del binario. Y porque está en el lenguaje, el mismo @table type se usa como request body de HTTP, como response, como argumento de User.insert(...), como elemento de List<User> que serializa a JSON — UNA fuente de verdad.



    Si tu proyecto cabe en Postgres y no necesitás composite PKs ni read replicas en el corto plazo, el ORM de Fitz cierra el ciclo "request HTTP → DB → response" con menos partes móviles y mejor performance que el stack típico Python.



    Si tu proyecto necesita uno de los items de "no está en el MVP", interop Python sigue siendo una opción válida (from python import sqlalchemy), o la deuda en cuestión vive en el roadmap.






    Esta cierra la serie de 9 posts: presentación, tutorial, deployment, CLI builder, WebSockets, cron jobs, auth, observability, y ORM. El stack entero. Lo que sigue son releases del lenguaje + el cap 31 de la guía (Postgres + ORM nativo) si querés profundizar.



    Repo:

    Doc dedicado de DB y ORM (~2600 LoC, 30 secciones):

    Vollständiger Original-Bericht
    Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
    ↗ Original-Artikel auf dev.to lesen
  • Wie bewertest du diesen Beitrag?
    1 Klick Feedback
    Teilen mit Netzwerk & Team:

    Community-Analysen & Experten-Meinungen 0

    Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
    Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
    Community Pulse: Relevanz-Einschätzung
    1 Klick Experten-Votum
    🔴 Akute Relevanz 0%
    🟡 In Evaluierung 0%
    🟢 Keine Auswirkung 0%
    Spannende Innovation 0%
    Verwandte Story-Cluster & Quellen (Vektor-KI)
    Port 8095 Engine
    1 Quelle
    Hackers Just Poisoned the Rust Supply Chain | Threat Wire
    1 Quelle
    Hackers Found a Way Into Humanoid Robots | Threat Wire
    1 Quelle
    Bits und so #1021 (Passwort für Laufwerk)
    Ähnliche Beiträge
    🔍 Verwandte News

    Auch interessante Nachrichten ORM tipado con migraciones automáticas: Fitz vs SQLAlchemy + Alembic + Pydantic

    Thematisch verwandte Begriffe: tipado, migraciones, automáticas, Fitz · 6 Treffer

    Laden...

    Videos werden geladen ...

    Laden...

    Beiträge werden geladen ...

    Laden...

    Videos werden geladen ...

    Laden...

    Beiträge werden geladen ...

    Laden...

    Videos werden geladen ...

    Laden...

    Beiträge werden geladen ...

    Laden...

    Videos werden geladen ...

    Laden...

    Beiträge werden geladen ...

    Laden...

    Videos werden geladen ...