Saltar a contenido

Novedades 4.0

Major release. Salto directo desde 3.3 — todo lo planificado para 3.4 (helpers framework-agnósticos, helpers PG, sibling packages djanorm-mypy y pytest-djanorm) viaja en este release, junto con las siete features headline que justifican el major bump.

1. Full-text search ampliado

Sobre la base existente (SearchVector, SearchQuery, SearchRank, SearchHeadline) se añaden:

from dorm.search import (
    TrigramSimilarity, TrigramWordSimilarity, search_index,
)

# Ranking por similitud trigram (requires pg_trgm extension)
qs = (
    Author.objects
    .annotate(score=TrigramSimilarity("name", "alise"))
    .order_by("-score")
)

# Indice GIN funcional para acelerar __search lookups
from dorm.migrations.operations import RunSQL
operations = [
    RunSQL(
        search_index("articles", "title", "body"),
        reverse_sql='DROP INDEX IF EXISTS ix_articles_search'
    ),
]

2. Migraciones online (zero-downtime)

from dorm.migrations.operations import (
    AddFieldOnline, BackfillBatch, SetNotNullOnline,
)

operations = [
    # 1. Añade columna nullable — no rewrite.
    AddFieldOnline(
        "Order",
        "currency",
        dorm.CharField(max_length=3, null=False, default="USD"),
    ),
    # 2. Backfill por chunks de PK.
    BackfillBatch(
        table="orders",
        update_sql=(
            'UPDATE "orders" SET "currency" = \'USD\' '
            'WHERE "id" BETWEEN %s AND %s AND "currency" IS NULL'
        ),
        batch_size=10_000,
        sleep_seconds=0.05,
    ),
    # 3. SET NOT NULL sin rewrite (PG ≥ 12 valida vía CHECK NOT VALID).
    SetNotNullOnline("Order", "currency"),
]

3. OpenTelemetry enriquecido

dorm.contrib.otel.instrument() ahora añade atributos siguiendo las SemConv 1.20+:

  • db.operation — verbo (SELECT/INSERT/UPDATE/DELETE/COPY/...)
  • db.sql.table y db.collection.name — tabla principal
  • db.dorm.alias — alias de conexión (multi-DB filtering)
  • Span name: "<OPERATION> <table>" (Datadog/Honeycomb agrupa).
from dorm.contrib.otel import instrument
instrument(tracer_name="myapp.dorm")

4. CTEs recursivos (árboles)

from dorm.tree import descendants, ancestors, descendants_cte

# Walks the adjacency list rooted at category id=42.
rows = descendants(Category, parent_field="parent_id", root_pk=42)

# Compose into a manual queryset:
cte = descendants_cte(
    Category,
    parent_field="parent_id",
    root_pk=42,
    cycle_field="path",  # cycle protection (PG-only)
)
qs = Category.objects.with_cte(descendants=cte).raw(
    'SELECT id, name FROM descendants ORDER BY id'
)

5. Multi-tenancy a nivel fila

Alternativa a dorm.contrib.tenants (schema-level PG-only):

import dorm
from dorm.contrib.tenants_row import TenantModel, current_tenant

class Note(TenantModel):
    title = dorm.CharField(max_length=200)

# En el handler / job:
with current_tenant(request.user.tenant_id):
    Note.objects.create(title="hi")            # tenant_id auto-fill
    notes = list(Note.objects.all())           # filtro auto

# Escape hatch
all_notes = list(Note.unscoped.all())

Funciona en PG, MySQL, SQLite, libsql. NoActiveTenantError cuando no hay tenant activo (no fallback silencioso).

6. Backend DuckDB

Primer backend OLAP analítico de la familia. Embebido, sin servidor, columnar, ejecución vectorizada.

import dorm

dorm.configure(
    DATABASES={"default": {"ENGINE": "duckdb", "NAME": "analytics.duckdb"}},
    INSTALLED_APPS=["dashboards"],
)

Soporta: - Atomic transactions (sin SAVEPOINT — DuckDB no lo tiene; nested atomic() degrada a no-op boundary). - Streaming iterator. - Introspection vía information_schema (compatible con dorm diff). - Async wrapper que ejecuta llamadas síncronas en thread executor.

Instalación: pip install 'djanorm[duckdb]'.

7. HStoreField + PG ENUM nativo

import enum, dorm

class Status(enum.Enum):
    ACTIVE = "active"
    ARCHIVED = "archived"

class Article(dorm.Model):
    # Storage hstore en PG, JSON-en-TEXT en SQLite.
    metadata = dorm.HStoreField(null=True, blank=True)
    # Tipo ENUM nativo PG (CREATE TYPE ... AS ENUM).
    status = dorm.EnumField(Status, native=True, type_name="article_status")

Migration matching:

from dorm.migrations.operations import (
    CreatePGEnum, DropPGEnum, AddPGEnumValue, CreateModel, RunSQL,
)

operations = [
    RunSQL("CREATE EXTENSION IF NOT EXISTS hstore",
           reverse_sql="DROP EXTENSION IF EXISTS hstore"),
    CreatePGEnum("article_status", ["active", "archived"]),
    CreateModel("Article", fields=[...]),
]

# Más adelante, para añadir un valor:
operations = [AddPGEnumValue("article_status", "deleted")]

HStoreField en SQLite hace fallback automático a TEXT JSON. EnumField(native=True) en SQLite/MySQL hace fallback a VARCHAR.

Sibling packages

Ver paquetes hermanos:

  • djanorm-mypy — plugin de mypy validando kwargs filter(), suffixes lookup, sintetizando pk/id.
  • pytest-djanorm — fixtures transactional_db, atransactional_db, pg_container, nplusone_guard.

Sacados a paquetes propios para mantener el wheel principal sin dependencias dev-only.

Migración desde 3.3.0

Cero cambios obligatorios. Todo opt-in:

  • pip install --upgrade djanorm — runtime sigue igual.
  • Si quieres DuckDB: pip install 'djanorm[duckdb]'.
  • Si quieres mypy/pytest tooling: pip install djanorm-mypy pytest-djanorm.
  • Si quieres helpers v4: from dorm.tree import ..., from dorm.contrib.tenants_row import ..., etc.