Saltar a contenido

Novedades en 4.2

Versión menor. Sin cambios incompatibles vs 4.1 — toda novedad es opt-in.

Contenido

  1. PG advisory locks
  2. EXPLAIN automático para slow queries
  3. SQL real en dorm sqlmigrate
  4. Stats agregadas por template
  5. Correlación logs ↔ trazas OTel
  6. ALTER COLUMN TYPE con lock acotado
  7. Rollback dry-run
  8. Rotación claves EncryptedField
  9. PII auto-mask en streaming
  10. Tablas append-only
  11. Saturación pool
  12. Circuit breaker replicas
  13. Grafo migraciones
  14. dorm reset
  15. cached() sugar
  16. DEBUG_NPLUSONE auto
  17. DataLoader async
  18. Plan drift
  19. LISTEN/NOTIFY broadcaster
  20. F.apply() chainable
  21. QuerySet.lookup()
  22. Manager.union_with()
  23. SQL allow-list
  24. dorm version
  25. dorm doctor auditorías v4.2
  26. querystats en metrics_response
  27. DataLoader prime() / clear_all()
  28. Plan-drift async
  29. Helpers capture-mode de allow-list

1. PG advisory locks

from dorm.contrib.advisory import (
    advisory_lock,
    try_advisory_lock,
    advisory_xact_lock,
)

# Bloquea hasta adquirir; libera al salir del bloque.
with advisory_lock("nightly-report"):
    run_nightly_report()

# Variante no bloqueante:
with try_advisory_lock("nightly-report") as got:
    if got:
        run_nightly_report()

# Alcance de transacción — se libera en commit/rollback.
with dorm.transaction.atomic():
    with advisory_xact_lock(("orders", 42)):
        ...

Claves: int / str (hash blake2b-8 determinista) / tupla (int, int). Async: aadvisory_lock / atry_advisory_lock.

2. EXPLAIN automático para slow queries

dorm.configure(
    SLOW_QUERY_MS=200,
    SLOW_QUERY_EXPLAIN=True,
    ...,
)

Toda query > 200 ms se re-explica automáticamente. El plan se loguea en dorm.db.slow_explain (WARNING) y se adjunta como evento al span OTel activo. Solo SELECT / WITH; mutaciones omitidas.

3. SQL real en dorm sqlmigrate

dorm sqlmigrate <app> <migration> ahora captura el DDL real vía la conexión dry-run (antes solo volcaba describe()).

4. Stats agregadas por template (Prometheus + JSON)

from dorm.contrib import querystats

querystats.collector().enable()

return Response(querystats.render_text(), media_type="text/plain")
# o JSON:
return querystats.render_json()

Expone dorm_template_count / dorm_template_total_ms / dorm_template_{p50,p95,p99}_ms por template. Reservoir acotado (1000 muestras por defecto).

5. Correlación logs ↔ trazas OTel

import logging
from dorm.contrib.otel import install_log_correlation

install_log_correlation()
logging.basicConfig(
    format="%(asctime)s [trace=%(otel_trace_id)s span=%(otel_span_id)s] %(message)s"
)

Une logs y traces. No-op cuando OTel SDK no está instalado (placeholders "-" mantienen el formatter vivo).

6. ALTER COLUMN TYPE con lock acotado

from dorm.migrations.operations import AlterColumnTypeOnline

operations = [
    AlterColumnTypeOnline(
        "User", "age", "BIGINT",
        lock_timeout="5s",
        old_type="INTEGER",
    ),
]

PG-only. Envuelve el ALTER en transacción corta con SET LOCAL lock_timeout — aborta bajo contención en lugar de bloquear writers.

7. Rollback de migraciones dry-run

dorm migrate --dry-run 0042_drop_legacy

Imprime el SQL exacto del rollback sin ejecutarlo. MigrationExecutor.rollback(..., dry_run=True) devuelve tuplas (sql, params).

8. Rotación de claves de EncryptedField

from dorm.contrib.encrypted import rotate_encryption_keys

rotate_encryption_keys(User, batch_size=1000)
# Async:
await arotate_encryption_keys(User)

Re-encripta cada fila con la clave nueva en bloques dentro de atomic(). Callback progress opcional para hooks tipo tqdm.

9. PII auto-mask en streaming

from dorm.contrib.streaming import stream_jsonl, astream_jsonl

for chunk in stream_jsonl(qs, mask_pii=True):
    yield chunk

async for chunk in astream_jsonl(aqs, mask_pii=True):
    yield chunk

Campos pii=True se reemplazan con el sentinel antes de serializar.

10. Tablas append-only para auditoría

from dorm.migrations.operations import MakeTableAppendOnly

operations = [
    MakeTableAppendOnly("article_history"),
    MakeTableAppendOnly("audit_log", allow_delete=True),
]

Trigger DB-level que rechaza UPDATE / DELETE. PG (PL/pgSQL) y SQLite (RAISE(ABORT)). MySQL / DuckDB loguean warning y omiten.

11. Gauge + warning de saturación de pool

dorm.configure(POOL_SATURATION_WARN=0.85)

Métrica dorm_pool_saturation{alias} (= in_use / max_size). WARNING en dorm.contrib.prometheus.pool al superar umbral.

12. Circuit breaker para read replicas

LagAwareReadRouter(
    primary="primary",
    replicas=["r1", "r2"],
    failure_threshold=3,
    cooldown_seconds=30.0,
)

Tras N fallos consecutivos abre breaker por replica y omite incluso el probe durante cooldown.

13. Grafo de dependencias de migraciones

dorm migrations-graph --format=mermaid > graph.mmd
dorm migrations-graph --format=dot | dot -Tpng -o graph.png

Camina migraciones por AST sin importarlas — deps runtime ausentes no rompen la visualización.

14. dorm reset (solo dev)

dorm reset
dorm reset --force

Rollback a zero + re-aplica forward. Refuse a producción salvo --force.

15. Model.objects.cached(timeout=...)

hot = Article.objects.cached(timeout=60).filter(published=True)

Sugar de .all().cache(timeout=60).filter(...).

16. DEBUG_NPLUSONE auto-detector

dorm.configure(
    DEBUG=True,
    DEBUG_NPLUSONE="raise",
    DEBUG_NPLUSONE_THRESHOLD=10,
)

from dorm.contrib.nplusone import install_debug_global
install_debug_global()

Instala detector N+1 global. "raise" aborta la request; True / "log" loguea sin interrumpir.

17. Async DataLoader

from dorm.contrib.dataloader import DataLoader

loader = DataLoader(lambda pks: Author.objects.filter(pk__in=pks))

results = await asyncio.gather(*(loader.load(pk) for pk in pks))
# N loads concurrentes coalescen en UN fetch

Patrón DataLoader de Facebook — GraphQL resolvers, RPC fan-out.

18. Detección de plan drift

from dorm.contrib.plan_drift import record_baseline, compare, diff_text

record_baseline("orders.by_customer", sql, params=[1])

result = compare("orders.by_customer", sql, params=[1])
if result.drifted:
    alert.fire("plan drift:\n" + diff_text(result))

Costes, row-estimates y timings se eliminan antes de comparar — solo cambios estructurales (tipos de nodo, estrategias de scan) lo disparan.

19. LISTEN/NOTIFY broadcaster

from dorm.contrib.listen_notify import Broadcaster

async with Broadcaster(["orders"]) as bcast:
    async with bcast.subscribe("orders") as queue:
        async for n in queue:
            ...

Una conexión LISTEN sirve N suscriptores async con su queue acotada.

20. F.apply() chainable

from dorm.expressions import F
from dorm.functions import Lower, Trim, Substr

F("name").apply(Lower).apply(Trim).apply(Substr, 1, 10)

Lee top-down en vez de inside-out. Disponible en F y todas las subclases de Func.

21. QuerySet.lookup(column=None)

top10 = Article.objects.order_by("-score")[:10]
Comment.objects.filter(article_id__in=top10.lookup("pk"))

Sugar para Subquery(qs.values(column)).

22. Manager.union_with() polimórfico

Source.objects.union_with(Other.objects, all=True, order_by=["created_at"])
Source.objects.union_with(
    (Other.objects, {"label": F("title")}),
    all=True,
)

UNION entre modelos heterogéneos con mapping por rama.

23. SQL allow-list

from dorm.contrib.sql_allowlist import install, uninstall, rejected_templates

# Fase canary:
install(allow_list, raise_on_violation=False, allow_ddl=True)
print(rejected_templates())

# Fase enforcement:
install(curated_list, raise_on_violation=True, allow_ddl=False)

Rechaza queries fuera de la lista curada. Defensa en profundidad junto a validación de input.

24. dorm version

$ dorm version
djanorm 4.2.0

Línea trivial — para tickets de soporte y logs CI.

25. dorm doctor auditorías v4.2

dorm doctor ahora detecta misconfigs específicos de v4.2:

  • DEBUG_NPLUSONE activo fuera de modo DEBUG.
  • SLOW_QUERY_EXPLAIN=True con SLOW_QUERY_MS demasiado bajo.
  • POOL_SATURATION_WARN fuera del rango (0, 1).
  • LagAwareReadRouter con cooldown_seconds=0 (breaker desactivado).
  • sql_allowlist sin instalar, o con allow_ddl=True en producción.

Ejecuta antes de cada deploy y bloquea según exit code.

26. querystats en metrics_response

El snapshot por-template de dorm.contrib.querystats ahora se integra automáticamente en prometheus.metrics_response() cuando el collector está habilitado:

from dorm.contrib import prometheus, querystats

querystats.collector().enable()
return Response(prometheus.metrics_response(), media_type="text/plain")

27. DataLoader prime() / clear_all()

loader.prime(7, fetched_value)   # pre-cargar sin batch
loader.clear_all()               # drop completo

prime tras fan-out write evita SELECT redundante en resolver siguiente.

28. Plan-drift async

await plan_drift.arecord_baseline("orders.by_customer", sql, params=[1])
result = await plan_drift.acompare("orders.by_customer", sql, params=[1])

Simétrico al sync. Mismas baselines, mismo CompareResult.

29. Helpers capture-mode de allow-list

from dorm.contrib import sql_allowlist

# Canary:
sql_allowlist.install([], raise_on_violation=False, allow_ddl=True)
# ... tráfico real ...
sql_allowlist.dump_captured("allowlist.json")

# Enforcement (tras curar):
sql_allowlist.uninstall()
sql_allowlist.load_from_file(
    "allowlist.json",
    raise_on_violation=True,
    allow_ddl=False,
)

dump_captured escribe JSON con allowed + rejected; load_from_file reinstala desde el payload curado. allowed_templates() devuelve snapshot actual.


Cambios completos en el CHANGELOG del proyecto. Subir 4.1 → 4.2 no necesita modificar código.