feat: FK polimorfica Document -> {partida, cove, edocument} + backfill (T2025-09-004)

Reemplaza el matching fragil por nombre de archivo con FK reales:
- 3 FK nullables (CASCADE) en Document; resolucion central en save() por
  document_type + nombre (core.document_links), cubre toda ruta de creacion
  incluida la ingesta del microservicio; set explicito en create_vu_record.
- Comando backfill_document_links (idempotente, dry-run) para filas existentes.
- Lectura/descarga/borrado SIEMPRE por la FK (id); el nombre solo ESTABLECE la
  FK en save()/backfill. Prefetch con select_related(pedimento, fuente) sin N+1.
- Migraciones: 0004 (campos), 0005 (indices CONCURRENTLY IF NOT EXISTS, idempotente
  via SeparateDatabaseAndState), 0006 (ANALYZE document para estadisticas del planner).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 08:13:47 -06:00
parent 244bbcb21c
commit 2e7d78fd8b
11 changed files with 592 additions and 196 deletions

View File

@@ -0,0 +1,30 @@
# Generated by Django 5.2.3 on 2026-06-24 13:37
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('customs', '0020_estados_descarga_t2026_05_027'),
('record', '0003_document_vu'),
]
operations = [
migrations.AddField(
model_name='document',
name='cove',
field=models.ForeignKey(blank=True, db_index=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='customs.cove'),
),
migrations.AddField(
model_name='document',
name='edocument',
field=models.ForeignKey(blank=True, db_index=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='customs.edocument'),
),
migrations.AddField(
model_name='document',
name='partida',
field=models.ForeignKey(blank=True, db_index=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='customs.partida'),
),
]

View File

@@ -0,0 +1,55 @@
# Índices de las FK de sub-entidad en la tabla `document` (grande: ~5M filas en
# prod) con CREATE INDEX CONCURRENTLY para no bloquear escrituras.
#
# CONCURRENTLY no corre dentro de transacción (atomic=False) y NO es transaccional:
# si el proceso muere a mitad puede dejar un índice a medias. Por eso:
# - IF NOT EXISTS → el reintento es idempotente (no choca con "already exists").
# - SeparateDatabaseAndState → el índice se refleja en el estado del modelo
# (AddIndex) sin que Django intente recrearlo, manteniendo el estado consistente.
#
# Recuperación si un build quedó INVALID (kill DURANTE la construcción, no después):
# DROP INDEX IF EXISTS "<nombre>"; y reintentar python manage.py migrate record
from django.db import migrations, models
class Migration(migrations.Migration):
atomic = False
dependencies = [
('record', '0004_document_subentidad_fk'),
]
operations = [
migrations.SeparateDatabaseAndState(
database_operations=[migrations.RunSQL(
sql='CREATE INDEX CONCURRENTLY IF NOT EXISTS "document_partida_idx" ON "document" ("partida_id");',
reverse_sql='DROP INDEX IF EXISTS "document_partida_idx";',
)],
state_operations=[migrations.AddIndex(
model_name='document',
index=models.Index(fields=['partida'], name='document_partida_idx'),
)],
),
migrations.SeparateDatabaseAndState(
database_operations=[migrations.RunSQL(
sql='CREATE INDEX CONCURRENTLY IF NOT EXISTS "document_cove_idx" ON "document" ("cove_id");',
reverse_sql='DROP INDEX IF EXISTS "document_cove_idx";',
)],
state_operations=[migrations.AddIndex(
model_name='document',
index=models.Index(fields=['cove'], name='document_cove_idx'),
)],
),
migrations.SeparateDatabaseAndState(
database_operations=[migrations.RunSQL(
sql='CREATE INDEX CONCURRENTLY IF NOT EXISTS "document_edocument_idx" ON "document" ("edocument_id");',
reverse_sql='DROP INDEX IF EXISTS "document_edocument_idx";',
)],
state_operations=[migrations.AddIndex(
model_name='document',
index=models.Index(fields=['edocument'], name='document_edocument_idx'),
)],
),
]

View File

@@ -0,0 +1,22 @@
# Recalcula estadísticas del planner tras crear los índices de las FK (0005).
# CREATE INDEX CONCURRENTLY NO corre ANALYZE: sin estadísticas frescas de las
# columnas nuevas (partida_id/cove_id/edocument_id, casi todas NULL antes del
# backfill), el planner puede elegir un seq scan sobre la tabla `document` (~5M
# filas) para las consultas del prefetch en vez de usar el índice → endpoints
# muy lentos. Este ANALYZE lo previene en cada entorno.
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('record', '0005_document_subentidad_idx'),
]
operations = [
migrations.RunSQL(
sql='ANALYZE "document";',
reverse_sql=migrations.RunSQL.noop,
),
]