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-s62 | Se aplica migration 0024_kind_lifecycle_drift con wrangler d1 migrations apply --remote |
| Durante s61-s62 | Wrangler 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-14 | Detectado 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
- Datos: ~31 filas únicas → ~62 filas (todas duplicadas). Ratio aprox. ×2.
- UX: Quick Links duplicados visibles en la UI del workspace.
- Funcional: Ningún endpoint roto; los duplicados eran simplemente ruido de datos.
- Severidad: Media (datos inconsistentes, no pérdida ni indisponibilidad).
Remediación aplicada (PR #40)
1. Migration 0025_quick_links_unique.sql
-- 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 *
- Si la fila ya existe →
RETURNINGdevuelvenull→ respuesta 409 con mensaje claro. - Protege contra dobles POST del modal y re-seeds que se cuelen.
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
- Sesión pendiente: Actualizar
scripts/seed-d1-migrations.sqlcon entradas 0015-0025 para que el tracking de Wrangler quede consistente ywrangler d1 migrations applysea usable normalmente para futuras migrations. - Establecer como norma: nunca usar
wrangler d1 execute --filesin registrar manualmente end1_migrations, o bien migrar completamente al flujowrangler d1 migrations apply.
Patrón aprendido
D1 + Wrangler:
INSERT OR IGNOREno protege contra duplicados de contenido cuando el PK es AUTOINCREMENT. Siempre definir UNIQUE constraints en columnas de negocio (no solo en PK). UsarON CONFLICT(...) DO NOTHING RETURNING *+ check denullen el handler para devolver 409 semántico.
Véase también
- [[incident—20260514—quicklinks-duplicados-d1-migrations]]
- [[workspace-tech—bd—tecnico]]
- [[concept—workspace—incidents-index]]