# Base de datos: migraciones y semillas

Los archivos aplicados son historial de despliegues: no se renombran ni se editan
para cambiar su intención después de haberse ejecutado. Las correcciones se hacen
con una migración nueva.

## Migraciones

```bash
php bin/make-migration agregar_indice_a_colaboradores
php bin/migrate status
php bin/migrate up
php bin/migrate validate    # revisión estática, sin tocar la base
```

El nombre sigue `YYYY_MM_DD_HHMMSS_descripcion_en_snake_case.php` y el archivo
retorna un arreglo con las closures `up(PDO $pdo)` y `down(PDO $pdo)`. El
ejecutor conserva compatibilidad con las migraciones históricas que declaran
funciones globales.

Una migración modifica esquema. Catálogos, usuarios de prueba y escenarios
comerciales pertenecen a una semilla.

## Semillas

### Dónde está cada dato

Un archivo por tabla, y el nombre del archivo dice qué contiene. Para encontrar
los colaboradores no hay que buscar: están en `030_ColaboradoresSeeder.php`.

El **prefijo numérico fija el orden de ejecución**, así que el listado del
directorio y el orden real no pueden contradecirse:

| Prefijo | Contenido | Perfil |
|---|---|---|
| `0xx` | Núcleo, catálogos base, nómina base y reclutamiento | `base` |
| `1xx` | Módulos funcionales | `modulos` |
| `2xx` | Escenarios de demostración por compañía | `completo` |

Las clases de soporte están en `support/` y no son semillas:

| Archivo | Responsabilidad |
|---|---|
| `support/Seeder.php` | Clase base: `upsert()`, consultas de esquema y helpers compartidos |
| `support/ClavesDeNegocio.php` | Por qué columna se reconoce cada tabla |
| `support/SeederManifest.php` | Perfiles y resolución del orden |
| `support/SeederPipeline.php` | Ejecuta una lista e informa qué pasó |

### Comandos

```bash
php bin/seed                  # perfil base
php bin/seed base             # núcleo, catálogos y nómina base
php bin/seed modulos          # base + módulos funcionales
php bin/seed completo         # todo, incluidos los escenarios demo
php bin/seed CargosSeeder     # una sola semilla, por su nombre de clase
php bin/seed --list           # perfiles y semillas
php bin/seed --validate       # revisión estática
php bin/seed --verify-demo    # cobertura de datos por módulo
```

`completo` está pensado para una base desechable de desarrollo, prueba o
demostración: actualiza los registros demo para mantenerlos reproducibles.

Nombres heredados que siguen funcionando como alias: `all` y `demo-complete` y
`demo` apuntan a `completo`, y `modules` a `modulos`.

### Crear una semilla

```bash
php bin/make-seeder 026 dias_no_laborables festivos_extra
```

El primer argumento es el prefijo de orden, y el generador rechaza uno ya usado
para que dos semillas no queden sin orden definido entre sí.

La clase extiende `Seeder`, recibe `PDO` en el constructor y expone `run()`.

### Escribir por clave de negocio

`upsert()` decide entre insertar y actualizar **leyendo antes por la clave de
negocio de la tabla**, que se declara en `support/ClavesDeNegocio.php`. No usa
`INSERT ... ON DUPLICATE KEY UPDATE`.

Esta es la regla que hace reproducible la siembra. El patrón anterior daba por
hecho que la única clave era `Id`; cuando la tabla tenía otra clave única
(`colaboradores.NoDocumento`, `cargos.Nombre`, `ceco.Ceco`,
`potencial_evaluaciones(PeriodoId, ColaboradorId)`), MySQL actualizaba una fila
distinta de la solicitada: el `Id` pedido no llegaba a existir y las filas hijas
fallaban por clave foránea o sobreescribían datos de otra semilla.

Consecuencias prácticas:

- **No fije un `Id` para una tabla cuya clave sea otra columna.** Deje que la
  tabla lo asigne y recoja el valor real del retorno de `upsert()`, que es un
  mapa `clave => Id`. Así lo hacen los escenarios con `cargos`, `ceco`,
  `potencial_evaluaciones` y `sucesion_puestos_clave`.
- Si declara un `Id` y la fila ya existe con otro, `upsert()` **falla** indicando
  ambos Id. Es un choque de rangos, no algo que deba resolverse pisando la fila.
- Una tabla que no exista provoca un error, no un salto silencioso: significa que
  falta una migración.

`ClavesDeNegocioTest` comprueba que cada clave declarada corresponde a un índice
`UNIQUE` real del esquema.

### Reparto de rangos de Id

Cada escenario escribe en su propio tramo, de modo que ninguno modifique las
filas de otro. Estos son los tramos que produce `php bin/seed completo`:

| Tabla | Base | Módulos / demo UI | Soluciones Andinas | Compañía Real Colombia |
|---|---|---|---|---|
| `colaboradores` | 1-20 | 20001-20020 | 200-249 | 300-313 |
| `areas` | 1-12 | 101-102 | 20-33 | — |
| `ceco` | 1-12 | 101-102 | por clave `Ceco` | — |
| `sedes` | 1-3 | 101-102 | 10-12 | — |
| `cargos` | 1-24 | — | por clave `Nombre` | reutiliza los base |
| `metas` | — | — | — | — |

El rango **20000-29999 de `colaboradores` está reservado** al padrón de
demostración que crea `105_ColaboradoresDemoSeeder`. `110_Evaluacion360Seeder`
solo opera sobre ese rango, para no generar evaluaciones sobre la nómina real;
la guarda la protege `Evaluacion360SeederSafetyTest`.

Cuando un escenario reutiliza una fila del catálogo base en lugar de crear la
suya (por ejemplo un cargo que ya existe con ese nombre), no reclama un `Id`:
traduce su Id local al real. `230_SolucionesAndinasSeeder` lo hace con `cargo()`
y `ceco()`.

### Idempotencia

Ejecutar un perfil dos veces seguidas debe dejar la base igual. Para que se
cumpla:

- Nada de `rand()` ni `array_rand()`: derive los valores del índice del bucle o
  de un Id, para que la misma fila salga igual en cada pasada.
- Si una fecha forma parte de la clave de negocio, ánclela a una fecha fija y no
  a `now`. Con `strtotime('-3 days')` la clave cambia en cada ejecución y la fila
  se intenta insertar de nuevo. Ver `FECHA_REFERENCIA` en
  `165_BeneficiosSeeder.php`.
- No borre para volver a insertar: `upsert()` con la clave correcta ya converge.

Diferencia esperada entre pasadas: las columnas de fecha de modificación se
refrescan, y `linea_etica_casos.PinHash` cambia porque `password_hash()` genera
una sal nueva cada vez.
