Volver a la wiki

Incident: Duplicados en quick_links por tracking roto de migrations D1 (2026-05-14)

Resumen ejecutivo

Duplicación masiva de filas en la tabla quick_links de Cloudflare D1 (workspace), causada por la re-aplicación silenciosa de migrations 0018-0019 al existir un desfase entre el estado real de la BD y lo registrado en d1_migrations. La tabla carecía de constraint UNIQUE en (label, url), por lo que los INSERT OR IGNORE de los seeds no protegieron contra duplicación de contenido (solo colisiones de PK/AUTOINCREMENT).

Estado: Resuelto en código (PR #40, 2026-05-14). Pendiente aplicar migration en remote.


Cronología

Hora (aprox.)Evento
Sesiones s61-s62Se aplica migration 0024_kind_lifecycle_drift con wrangler d1 migrations apply --remote
Durante s61-s62Wrangler detecta 0018-0019 como “pendientes” (no estaban en d1_migrations) y las re-aplica silenciosamente
Post s61-s62~31 filas en quick_links → ~62 (todos los entries seedeados duplicados)
2026-05-14Detectado y corregido en PR #40 (sesión 64)

Causa raíz

Causa primaria — tracking gap en d1_migrations

El script scripts/seed-d1-migrations.sql solo registró las migrations 0001-0014 en la tabla de tracking de Wrangler. Las migrations 0015-0024 fueron aplicadas directamente (con wrangler d1 execute --file o equivalente) sin registrarlas en d1_migrations.

Cuando se invocó wrangler d1 migrations apply --remote para aplicar la 0024, Wrangler comparó los archivos en migrations/ contra d1_migrations y consideró pendientes 0018 y 0019, re-aplicándolas sin advertencia visible.

Causa secundaria — ausencia de UNIQUE constraint

La tabla quick_links no tenía ningún índice o constraint UNIQUE en las columnas de contenido. Los seeds de 0018/0019 usan INSERT OR IGNORE, que solo ignora colisiones sobre la Primary Key (id INTEGER AUTOINCREMENT). Al re-insertarse, cada fila recibió un nuevo id → ningún conflicto → duplicado silencioso.


Impacto


Remediación aplicada (PR #40)

-- Paso 1: Deduplicar conservando la fila más antigua (menor id) de cada (label, url)
DELETE FROM quick_links
WHERE id NOT IN (
  SELECT MIN(id) FROM quick_links GROUP BY label, url
);

-- Paso 2: UNIQUE INDEX para prevenir recurrencia
CREATE UNIQUE INDEX IF NOT EXISTS idx_quick_links_label_url_unique
  ON quick_links(label, url);

La migration es idempotente: se puede re-ejecutar sin efecto adverso.

2. Endpoint POST /api/quicklinks — index.ts

INSERT INTO quick_links (label, url, ...)
VALUES (?, ?, ...)
ON CONFLICT(label, url) DO NOTHING
RETURNING *

3. Endpoint PUT /api/quicklinks/[id] — [id].ts

Try/catch alrededor del UPDATE ... RETURNING *. Si el error contiene 'UNIQUE constraint failed' → respuesta 409 con mensaje claro. Cualquier otro error se re-lanza.


Pasos de remediación pendientes en producción

⚠️ Acción manual requerida — no se completa con el merge.

# Aplicar la migration de limpieza + UNIQUE INDEX en remote
wrangler d1 execute crearacksl-workspace-db --remote \
  --file=migrations/0025_quick_links_unique.sql

# Verificar: debe quedar ~25-30 filas (antes ~62)
wrangler d1 execute crearacksl-workspace-db --remote \
  --command="SELECT COUNT(*) FROM quick_links;"

Backlog derivado


Patrón aprendido

D1 + Wrangler: INSERT OR IGNORE no protege contra duplicados de contenido cuando el PK es AUTOINCREMENT. Siempre definir UNIQUE constraints en columnas de negocio (no solo en PK). Usar ON CONFLICT(...) DO NOTHING RETURNING * + check de null en el handler para devolver 409 semántico.


Véase también

Subir