|
|
|
@@ -0,0 +1,165 @@
|
|
|
|
|
## 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_<accion>.sql`. Numeracion zero-padded de 3 digitos. Nombre descriptivo.
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
apps/<app>/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_<accion>.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/<app>/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_<accion>.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.
|