## Migraciones de BBDD: nunca perder datos **Regla absoluta:** todo cambio de schema en SQLite (apps con `kanban.db`, `operations.db` propia, registry.db, etc.) DEBE ir en un archivo de migración versionado. Nunca borrar/recrear tablas, nunca cambiar tipos sin proceso seguro, nunca confiar en "borra el .db y vuelve a empezar". ### Por que - Las apps almacenan **datos vivos** (cards, entities, executions, assertions, columns, sessions). - Borrar = perder horas/dias/semanas de trabajo del usuario. - Lo que es trivial en dev (`rm operations.db`) es destructivo en produccion (deploys + sync entre PCs). - Sync entre PCs (`fn sync`, `/full-git-pull`) trae bases de datos de otros equipos: si tu schema asume tabla recreada, los datos del otro PC desaparecen. ### Patrones obligatorios #### 1. Archivos numerados en `migrations/` Cada cambio de schema = un archivo nuevo `migrations/NNN_.sql`. Numeracion zero-padded de 3 digitos. Nombre descriptivo. ``` apps//migrations/ 001_init.sql # CREATE TABLE inicial (no se modifica nunca) 002_add_stickers.sql # ALTER TABLE cards ADD COLUMN stickers 003_add_assignees.sql # ALTER TABLE cards ADD COLUMN assignee_id 004_create_lock_history.sql # CREATE TABLE card_lock_history ... ``` #### 2. Solo operaciones aditivas seguras | Operacion | Seguro | Notas | |---|---|---| | `CREATE TABLE IF NOT EXISTS` | si | idempotente | | `CREATE INDEX IF NOT EXISTS` | si | idempotente | | `ALTER TABLE ... ADD COLUMN` | si | aditivo, default obligatorio | | `INSERT INTO ... ON CONFLICT IGNORE` | si | seed data idempotente | | `DROP TABLE` | NO | destructivo | | `DROP COLUMN` | NO | destructivo (SQLite < 3.35 ni siquiera lo soporta) | | `ALTER TABLE ... RENAME COLUMN` | precaucion | rompe codigo viejo si rollback | | `ALTER TABLE ... DROP/ALTER constraint` | NO sin backup | requiere recreate-and-copy | Si necesitas cambiar tipo, eliminar columna, o cambiar PK: hacer **migracion en pasos** (Branch by Abstraction): 1. Crear nueva columna/tabla con la forma deseada (migration N). 2. App escribe en ambas (migration N+1, codigo). 3. Backfill de datos viejos (migration N+2, script). 4. App lee solo de la nueva (migration N+3, codigo). 5. Eliminar la vieja (migration N+4, despues de tener backups verificados). Cada paso = una rama TBD corta + commit + verificacion. Nunca un solo PR que rompa lectores. #### 3. Aplicacion idempotente al arrancar La app aplica todas las migraciones en orden al iniciar. Patron canonico (Go): ```go //go:embed migrations/*.sql var migrationsFS embed.FS func applyMigrations(conn *sql.DB) error { files, err := fs.Glob(migrationsFS, "migrations/*.sql") if err != nil { return err } sort.Strings(files) for _, f := range files { b, err := migrationsFS.ReadFile(f) if err != nil { return err } if _, err := conn.Exec(string(b)); err != nil { // SQLite ALTER TABLE ADD COLUMN no es idempotente nativamente. // Si ya existe, ignorar el error de "duplicate column". if !strings.Contains(err.Error(), "duplicate column") && !strings.Contains(err.Error(), "already exists") { return fmt.Errorf("%s: %w", f, err) } } } return nil } ``` Alternativa: tabla `_migrations` con las versiones aplicadas (mas robusta para schemas grandes). Para apps pequeñas (kanban, operations.db), bastan los archivos numerados + `IF NOT EXISTS` / catch de "duplicate column". #### 4. Migracion + cambios en codigo en el mismo commit Cuando añades una columna: - `migrations/NNN_.sql` (nueva) - `db.go` (lee/escribe la columna) - `types.ts` (frontend type) - Tests Todo en el mismo commit/rama. Si solo mergeas la migracion pero no el codigo, otros PCs aplican la migracion al sync y luego el codigo viejo no la usa. OK. Si mergeas el codigo sin la migracion, la app peta al arrancar en otros PCs. Mal. **Migracion antes que codigo en el orden de archivos** (no de tiempo). #### 5. Tests sobre la migracion Cada migracion debe tener test que: - Arranca con DB vacia → aplica todas → verifica schema. - Arranca con DB en estado N-1 (datos previos) → aplica migracion N → verifica que los datos se conservan. Esto detecta migraciones destructivas antes de mergear. ### Que NO hacer | Anti-patron | Consecuencia | |---|---| | Borrar `*.db` durante dev y commitear "schema actualizado" | Otros PCs pierden datos al sync. | | Modificar `001_init.sql` para añadir columnas | Las DBs ya creadas no se actualizan. Datos divergentes. | | `DROP TABLE x; CREATE TABLE x ...` | Borra todo lo que el usuario tenga. | | Usar `ensureColumns` sin archivo SQL paralelo | El cambio de schema vive solo en codigo Go, no auditable, no migrable manualmente. | | Cambiar tipo de columna in-place | SQLite necesita recreate-and-copy. Asume que pierde datos si no se hace bien. | | "fn index" como solucion para regenerar registry.db | OK para `registry.db` (regenerable). NUNCA para `operations.db`, `kanban.db`, etc. | ### Casos especiales #### registry.db (raiz del fn_registry) `registry.db` SE PUEDE regenerar con `fn index` desde los `.go` y `.md`. Para cambios de schema del registry: actualizar `registry/migrations.go` o el codigo de creacion + `fn index`. NO hace falta archivo de migracion porque la fuente de verdad son los `.md`/`.go`. Excepcion: tablas con datos vivos (`proposals`, `pc_locations`) — esas SI requieren migracion preservando datos. #### operations.db (por app) Cada app tiene su `operations.db` con entities/relations/executions. Schema definido en `fn_operations/`. Cambios al schema → archivo de migracion en `fn_operations/migrations/` aplicado al abrir la BD. Idempotente. #### apps con BD propia (kanban, etc.) Mismo patron: `apps//migrations/NNN_*.sql`, embebido y aplicado al arrancar. ### Comandos utiles ```bash # Ver schema actual sqlite3 apps/kanban/operations.db ".schema" # Ver columnas de una tabla sqlite3 apps/kanban/operations.db "PRAGMA table_info(cards);" # Backup antes de migracion arriesgada sqlite3 apps/kanban/operations.db ".backup apps/kanban/operations.db.bak.$(date +%Y%m%d)" # Aplicar una migracion manual (si la app no esta corriendo) sqlite3 apps/kanban/operations.db < apps/kanban/migrations/00X_.sql # Listar archivos de migracion en orden ls apps/kanban/migrations/*.sql | sort ``` ### Resumen - Cada cambio de schema = archivo numerado nuevo en `migrations/`. - Aditivo siempre que se pueda. Destructivo solo en pasos verificados con backup. - App aplica migraciones al arrancar, idempotente. - Migracion + codigo + tests en el mismo commit. - Nunca borrar `.db` para "arreglar" schema. Nunca modificar migraciones existentes. ### Estado retroactivo (2026-05-09) Inventario de BDs del ecosistema y conformidad con la regla: | Repo / App | BD | `migrations/` | Estado | |---|---|---|---| | `registry/` | `registry.db` | si (11 archivos) | ✓ | | `fn_operations/` | `operations.db` por app | si (4 archivos) | ✓ | | `apps/kanban/` | `operations.db` (kanban) | si (5 archivos: 001 init, 002 stickers, 003 columns_extras, 004 cards_extras, 005 history_actor) | ✓ | | `apps/deploy_server/` | `operations.db` (deploys) | si (2 archivos: 001 init, 002 target_extras) | ✓ | | `apps/dag_engine/store/` | DB del dag_engine | si (001_init) | ✓ | | `projects/element_agents/.../shell/memory/` | memoria del agente | si (001_init) | ✓ | | `projects/osint_graph/apps/graph_explorer/` | DBs C++ inline (project_manager, layout_store, jobs, node_groups) | NO | **pendiente** — refactor C++ multi-archivo, mover schema inline a `migrations/*.sql` aplicado al abrir cada DB. | Las apps marcadas ✓ usan el patron canonico `embed.FS + applyMigrations()` (Go) o equivalente. La C++ pendiente requiere ronda dedicada — tracker via issue cuando se aborde. `apps/kanban/db.go::ensureColumns` se mantiene como **backstop idempotente** para DBs muy antiguas creadas antes del refactor de migraciones. NO añadir columnas nuevas alli — siempre via archivo SQL.