Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions .github/workflows/database.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,23 @@ name: database
#
# También se dispara cuando cambia el mapeo JPA, porque es justo ahí donde
# el esquema escrito a mano y las entidades pueden divergir (ADR-0005).
#
# El esquema son las migraciones de Flyway (ADR-0006). Aquí se aplican con
# psql en orden de versión; que Flyway mismo las ejecute lo comprueban las
# pruebas de integración del workflow de build.

on:
push:
branches: [main]
paths:
- 'docs/database/**'
- 'src/main/resources/db/migration/**'
- 'src/main/java/**/persistence/**'
- '.github/workflows/database.yml'
pull_request:
paths:
- 'docs/database/**'
- 'src/main/resources/db/migration/**'
- 'src/main/java/**/persistence/**'
- '.github/workflows/database.yml'
workflow_dispatch:
Expand Down Expand Up @@ -54,6 +60,7 @@ jobs:
env:
PGPASSWORD: postgres
PSQL: psql -h localhost -U postgres -d reservas
MIGRACIONES: src/main/resources/db/migration

steps:
- uses: actions/checkout@v4
Expand All @@ -63,17 +70,26 @@ jobs:
command -v psql || sudo apt-get update && sudo apt-get install -y postgresql-client
psql --version

- name: El DDL ejecuta contra una base vacía
run: $PSQL -v ON_ERROR_STOP=1 -q -f docs/database/schema.sql
- name: Las migraciones ejecutan contra una base vacía
run: |
for f in $(ls $MIGRACIONES/V*__*.sql | sort -V); do
echo "aplicando $f"
$PSQL -v ON_ERROR_STOP=1 -q -f "$f"
done

- name: Se crearon las 19 tablas
run: |
n=$($PSQL -tAc "select count(*) from pg_class where relnamespace = 'public'::regnamespace and relkind = 'r'")
echo "tablas creadas: $n"
test "$n" -eq 19

- name: El DDL es idempotente
run: $PSQL -v ON_ERROR_STOP=1 -q -f docs/database/schema.sql
# V1 y V2 se vuelven a aplicar sobre las bases creadas antes de Flyway
# (línea base 0), así que tienen que seguir siendo idempotentes. Las
# migraciones siguientes corren una sola vez y no necesitan serlo.
- name: V1 y V2 son idempotentes
run: |
$PSQL -v ON_ERROR_STOP=1 -q -f $MIGRACIONES/V1__esquema_inicial.sql
$PSQL -v ON_ERROR_STOP=1 -q -f $MIGRACIONES/V2__rls.sql

- name: Los datos de prueba cargan
run: $PSQL -v ON_ERROR_STOP=1 -q -f docs/database/seed.sql
Expand Down Expand Up @@ -101,9 +117,10 @@ jobs:
test "$rechazos" -eq 18
test "$coladas" -eq 0

# Ninguna tabla de public sin RLS, sean cuantas sean: una migración
# nueva que olvide habilitarlo deja la tabla expuesta por PostgREST.
- name: La seguridad de fila queda habilitada
run: |
$PSQL -v ON_ERROR_STOP=1 -q -f docs/database/rls.sql
n=$($PSQL -tAc "select count(*) from pg_class where relnamespace = 'public'::regnamespace and relkind = 'r' and relrowsecurity")
echo "tablas con RLS: $n"
test "$n" -eq 19
sin_rls=$($PSQL -tAc "select string_agg(relname, ', ') from pg_class where relnamespace = 'public'::regnamespace and relkind = 'r' and not relrowsecurity")
echo "tablas sin RLS: ${sin_rls:-ninguna}"
test -z "$sin_rls"
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ contraseña). MFA de administradores diferido a Sprint 2.
| ADR-0003 Autenticación JWT/JWKS + bloqueo propio | `docs/adr/ADR-0003-*.md` |
| ADR-0004 API versionada, ProblemDetail, traceId | `docs/adr/ADR-0004-*.md` |
| ADR-0005 Modelo físico escrito a mano + CI de BD | `docs/adr/ADR-0005-*.md` |
| ADR-0006 Migraciones de esquema con Flyway | `docs/adr/ADR-0006-*.md` |
| Guía de la carpeta de base de datos | `docs/database/README.md` |
| Modelo lógico completo (ER) | `docs/database/modelo-logico.md` |
| Modelo físico (DDL, 19 tablas) | `docs/database/schema.sql` |
| Modelo físico: migraciones de Flyway (V1 = las 19 tablas del Sprint 1) | `src/main/resources/db/migration/` |
| Datos de prueba | `docs/database/seed.sql` |
| Restricciones de integridad en acción | `docs/database/pruebas-integridad.sql` |
| Seguridad Supabase (RLS) | `docs/database/rls.sql` |
| Seguridad Supabase (RLS) | `src/main/resources/db/migration/V2__rls.sql` |
| Consultas clave del negocio | `docs/database/consultas-clave.md` |
| Consultas clave, versión ejecutable | `docs/database/consultas-clave.sql` |
| Diagrama ER editable | `docs/database/diagrama-er.drawio` |
Expand Down Expand Up @@ -145,10 +146,13 @@ Linux/macOS:

Notas:

- El esquema completo vive en `docs/database/schema.sql` y se aplica con
`psql -f` o desde el SQL Editor de Supabase (ADR-0005). `ddl-auto=update`
sigue creando en local las tablas que tienen entidad JPA. Después
ejecuta `docs/database/rls.sql`.
- El esquema lo crea Flyway al arrancar, con las migraciones de
`src/main/resources/db/migration/` (ADR-0006). Hibernate ya no crea tablas:
solo valida que las entidades cuadren con el esquema.
- La aplicación migra la base a la que apunta `DATABASE_URL`. Si tu `.env`
apunta a Supabase, al arrancar le aplicas las migraciones de tu rama a la
base compartida. Para desarrollar, usa el PostgreSQL de `docker compose up -d db`
(`DATABASE_URL=jdbc:postgresql://localhost:5432/postgres`).
- Swagger UI: http://localhost:8080/swagger-ui.html
- Health: http://localhost:8080/actuator/health

Expand Down Expand Up @@ -186,7 +190,8 @@ Errores: `application/problem+json` con `errorCode`, `traceId` y `details`.
## Despliegue en Render

Producción: `https://bookingplatform-81wi.onrender.com`
(Web Service Docker desde `main`, perfil `cloud`, `ddl-auto=validate`).
(Web Service Docker desde `main`, perfil `cloud`. Al arrancar, Flyway aplica
las migraciones pendientes a Supabase y Hibernate valida el esquema).

### Crear el servicio

Expand Down
2 changes: 1 addition & 1 deletion docs/adr/ADR-0002-supabase-postgres-auth.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0002: PostgreSQL administrado en Supabase como base de datos y proveedor de identidad

- **Estado:** Aceptado; la estrategia de esquema la modifica el ADR-0005
- **Estado:** Aceptado; la estrategia de esquema la modifica el ADR-0005 y la reemplaza el ADR-0006
- **Fecha:** 2026-09-18
- **Prioridad:** Alta
- **Decisores:** Equipo de desarrollo — rol Arquitecto de Software y BD
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/ADR-0005-modelo-fisico-escrito-a-mano.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ADR-0005: El modelo físico se escribe a mano y se verifica en integración continua

- **Estado:** Aceptado; modifica la estrategia de esquema del ADR-0002
- **Estado:** Aceptado; modifica la estrategia de esquema del ADR-0002. Reemplazado en ese punto por el ADR-0006: el esquema vive ahora en las migraciones de Flyway
- **Fecha:** 2026-09-21
- **Prioridad:** Alta
- **Decisores:** Equipo de desarrollo — rol Arquitecto de Software y BD
Expand Down
102 changes: 102 additions & 0 deletions docs/adr/ADR-0006-migraciones-flyway.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# ADR-0006: El esquema se versiona con migraciones de Flyway

- **Estado:** Aceptado; reemplaza la estrategia de esquema del ADR-0002 y del ADR-0005
- **Fecha:** 2026-10-24
- **Prioridad:** Alta
- **Decisores:** Equipo de desarrollo — rol Arquitecto de Software y BD

## Contexto

El ADR-0005 hizo de `docs/database/schema.sql` la fuente del modelo de datos,
escrito a mano y aplicado con `psql` o desde el SQL Editor de Supabase, y dejó
Flyway para el Sprint 2. Esa estrategia tiene tres limitaciones que el Sprint 2
vuelve urgentes:

1. **Las pruebas de integración no usaban el esquema real.** El perfil de
pruebas tenía `ddl-auto=create-drop`, así que Hibernate creaba las tablas a
partir de las entidades. Los triggers, las restricciones `exclude` y los
`check` de `schema.sql` no actuaban en ninguna prueba de la aplicación; solo
los comprobaba el workflow de base de datos, por separado.
2. **Un único archivo editado por varias personas.** HU-002, 003, 004, 006 y
009 cambian el esquema a la vez (agendas, mantenimientos, estados de
servicio, documento del proveedor). Sobre un solo `schema.sql`, esos cambios
chocan, y nada registra qué versión tiene aplicada cada base.
3. **El despliegue dependía de un paso manual.** Había que acordarse de aplicar
el script en Supabase antes de desplegar; si se olvidaba, el `validate` del
perfil `cloud` tumbaba el arranque.

## Decisión

Adoptar Flyway, integrado en Spring Boot, como la única vía de cambio del
esquema.

1. **Las migraciones son la fuente del modelo físico** y viven en
`src/main/resources/db/migration/`. `V1__esquema_inicial.sql` es el
`schema.sql` del Sprint 1, con el mismo DDL y el encabezado al día.
`V2__rls.sql` es el `rls.sql`, también sin cambios en lo que ejecuta. Una
migración aplicada no se edita nunca: los cambios van en una versión nueva.
2. **Hibernate solo valida.** `ddl-auto=validate` en todos los perfiles,
pruebas incluidas, así que las pruebas de integración corren sobre el
esquema que construye Flyway, con sus triggers y restricciones.
3. **Línea base 0 para las bases anteriores a Flyway.** Supabase y las bases
locales ya tienen el esquema pero no la tabla de historial. Con
`baseline-on-migrate` y `baseline-version: 0`, Flyway las marca como
anteriores a V1 y aplica V1 y V2 encima. Como los dos scripts son
idempotentes desde el ADR-0005, se saltan lo que ya existe y crean lo que
falte. Una base vacía se migra desde V1 sin línea base.
4. **RLS también para el historial de Flyway.** Flyway crea
`flyway_schema_history` en `public`, y en Supabase toda tabla nueva de
`public` hereda permisos para `anon` y `authenticated`. No puede ir en una
migración, porque Flyway tiene bloqueada esa tabla mientras migra y el
`alter table` esperaría para siempre. La cubre el callback
`afterMigrate__rls_historial_flyway.sql`, que corre al terminar cada
`migrate`, cuando ya soltó el bloqueo, y es idempotente. Cada
migración que cree tablas habilita RLS en ellas; `DatabaseMigrationsIT` y
el workflow de base de datos fallan si alguna tabla de `public` queda sin RLS.
5. **El despliegue migra.** En Render, la aplicación aplica las migraciones
pendientes al arrancar, antes de que Hibernate valide. Se acabó el paso
manual en el SQL Editor.

## Alternativas consideradas

1. **Línea base 1 (`baseline-version: 1`):** es la configuración habitual,
pero da V1 por aplicada sin ejecutarla. Si una base hubiera quedado a medias
(por ejemplo, sin el trigger de motivo o sin RLS), la diferencia quedaría
oculta para siempre. Con la línea base 0 y scripts idempotentes, V1 y V2
completan lo que falte.
2. **Ejecutar las migraciones con el plugin de Maven o la CLI de Flyway, aparte
de la aplicación:** separa el despliegue del esquema del de la aplicación,
pero exige credenciales de Supabase en CI y un paso más que coordinar en
Render. Con una sola instancia y migraciones pequeñas, migrar al arrancar
es más simple y no deja ventanas en que el código y el esquema discrepen.
3. **Liquibase:** resuelve lo mismo, pero sus *changelogs* en XML o YAML
ocultan el SQL, y este proyecto depende de SQL específico de PostgreSQL
(`exclude using gist`, rangos, triggers en PL/pgSQL). Flyway ejecuta SQL
plano, que es lo que el equipo ya escribe y revisa.

## Consecuencias

- (+) Las pruebas de integración ejercitan las reglas de la base de datos. Un
caso de uso que viole un trigger o una restricción falla en las pruebas, no
en producción.
- (+) Cada cambio de esquema es un archivo revisable en su pull request, y cada
base registra qué versión tiene.
- (+) Desplegar ya no exige pasos manuales en Supabase.
- (−) V1 y V2 tienen que seguir siendo idempotentes, porque se vuelven a
aplicar sobre las bases anteriores a Flyway. El workflow de base de datos
los ejecuta dos veces para comprobarlo. Las migraciones siguientes no lo
necesitan.
- (−) La aplicación migra la base a la que apunta `DATABASE_URL`. Arrancarla
en local apuntando a Supabase con una rama que traiga migraciones nuevas las
aplica a la base compartida antes de la revisión. El desarrollo local va
contra el PostgreSQL de `docker compose`.
- (−) Ya no hay un único archivo con el esquema completo. Para leerlo entero
están V1, las migraciones siguientes en orden y `modelo-logico.md`.
- (−) Una migración que falle en Render deja la instancia sin arrancar. Por
eso cada migración se prueba primero en las pruebas de integración y en el
workflow de base de datos, sobre PostgreSQL 16.

Ver `src/main/resources/db/migration/` (las migraciones),
`docs/database/README.md` (cómo trabajar con ellas),
`CloudProfileContextIT` (la primera migración de Supabase, simulada) y
`DatabaseMigrationsIT` (una base vacía migrada desde V1).
72 changes: 54 additions & 18 deletions docs/database/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,31 @@

Qué hay aquí, en qué orden se ejecuta y cómo levantarlo para trabajar.

## Los archivos
## Dónde está el esquema

El modelo físico son las migraciones de Flyway en
`src/main/resources/db/migration/` (ADR-0006). La aplicación las aplica al
arrancar, en orden de versión:

| Migración | Qué hace |
|---|---|
| `V1__esquema_inicial.sql` | Las diecinueve tablas del Sprint 1 con sus restricciones, triggers e índices. Es el antiguo `schema.sql` (ADR-0005) |
| `V2__rls.sql` | Habilita Row Level Security en las diecinueve tablas. Es el antiguo `rls.sql` |
| `afterMigrate__rls_historial_flyway.sql` | Callback, no migración: corre al final de cada `migrate` y habilita RLS en `flyway_schema_history`, que Flyway tiene bloqueada mientras migra |

V1 y V2 no se modifican nunca: Flyway guarda su checksum y no arranca si
cambian. Un cambio de esquema es una migración nueva, `V3__<descripcion>.sql`
y siguientes. Si crea tablas, habilita RLS en ellas en la misma migración;
`DatabaseMigrationsIT` y el workflow de base de datos fallan si alguna tabla de
`public` queda sin RLS.

## Los archivos de esta carpeta

| Archivo | Qué es | ¿Se ejecuta? |
|---|---|---|
| `schema.sql` | Modelo físico. Diecinueve tablas con sus restricciones e índices. Es la fuente del esquema (ADR-0005) | Sí, primero |
| `seed.sql` | Datos de prueba. Tres organizaciones, cinco servicios, ocho reservas | Sí, después del esquema |
| `seed.sql` | Datos de prueba. Tres organizaciones, cinco servicios, ocho reservas | Sí, después de las migraciones |
| `consultas-clave.sql` | Las catorce preguntas de negocio, ejecutables de corrido | Sí, para ver resultados |
| `pruebas-integridad.sql` | Dieciocho instrucciones que violan una regla cada una y deben ser rechazadas | Sí, para comprobar |
| `rls.sql` | Habilita Row Level Security en las diecinueve tablas | Sí, al final |
| `modelo-logico.md` | Diagrama entidad-relación, decisiones de modelado y normalización | No, se lee |
| `consultas-clave.md` | Las mismas catorce preguntas con su explicación | No, se lee |
| `diagrama-er.drawio` | Diagrama editable. La primera página, **Modelo completo**, tiene las diecinueve entidades, las notas y las historias de usuario en una sola hoja; las cuatro siguientes son las tres vistas por dominio y la hoja de historias de usuario | No |
Expand All @@ -22,12 +38,16 @@ ejecutan con `psql -f` y no pegándolos en el editor SQL de Supabase.

## Levantarlo en local

El `docker-compose.yml` de la raíz ya trae un PostgreSQL 16. No hace falta nada
más:
El `docker-compose.yml` de la raíz ya trae un PostgreSQL 16. Arranca la
aplicación contra él (`DATABASE_URL=jdbc:postgresql://localhost:5432/postgres`)
y Flyway crea el esquema. Si solo quieres el esquema y los datos, sin la
aplicación, aplica las migraciones a mano en orden de versión:

```bash
docker compose up -d db
docker compose exec -T db psql -U postgres -d postgres -v ON_ERROR_STOP=1 < docs/database/schema.sql
for f in $(ls src/main/resources/db/migration/V*__*.sql | sort -V); do
docker compose exec -T db psql -U postgres -d postgres -v ON_ERROR_STOP=1 < "$f"
done
docker compose exec -T db psql -U postgres -d postgres -v ON_ERROR_STOP=1 < docs/database/seed.sql
docker compose exec -T db psql -U postgres -d postgres < docs/database/consultas-clave.sql
```
Expand All @@ -43,29 +63,45 @@ Cuando termines: `docker compose down -v` borra también el volumen.
**`seed.sql` empieza con un `truncate`.** Tiene una guarda que aborta si la base
no parece local, pero aun así no lo ejecutes contra la base compartida.

## Aplicarlo en Supabase
Si aplicaste las migraciones a mano y después arrancas la aplicación contra
esa misma base, no pasa nada: Flyway la marca con la línea base 0 y vuelve a
aplicar V1 y V2, que son idempotentes.

`schema.sql` es idempotente: se salta `clients` y `login_attempts`, que ya
existen porque las crea el mapeo JPA, y crea las diecisiete restantes sin tocar
ninguna columna de las dos primeras. Después hay que ejecutar `rls.sql`, porque
sin él las tablas nuevas quedan expuestas por PostgREST con el anon key, que es
público (ADR-0002).
## Supabase

Ya no se ejecuta nada a mano en el SQL Editor. En cada despliegue, la
aplicación aplica a Supabase las migraciones pendientes. La primera vez que
arranque con Flyway, la base ya tiene el esquema del Sprint 1 pero no el
historial: Flyway la marca con la línea base 0 y vuelve a aplicar V1 y V2, que
se saltan lo que ya existe (lo comprueba `CloudProfileContextIT`).

Por lo mismo, no arranques la aplicación desde tu máquina apuntando a Supabase
con una rama que traiga migraciones nuevas: se aplicarían a la base compartida
antes de la revisión.

No ejecutes `seed.sql` ni `pruebas-integridad.sql` contra Supabase.

## Qué verifica la integración continua

El workflow `.github/workflows/database.yml` corre en cada pull request que
toque esta carpeta o el mapeo JPA, y comprueba que el DDL ejecute contra una
base vacía, que sea idempotente, que se creen las diecinueve tablas, que los
datos de prueba carguen, que las catorce consultas devuelvan filas y que las
dieciocho pruebas de integridad sean rechazadas.
toque esta carpeta, las migraciones o el mapeo JPA. Comprueba que:

- las migraciones ejecuten contra una base vacía;
- V1 y V2 sigan siendo idempotentes;
- se creen las diecinueve tablas;
- los datos de prueba carguen;
- las catorce consultas devuelvan filas;
- las dieciocho pruebas de integridad sean rechazadas;
- ninguna tabla quede sin RLS.

Las pruebas de integración del build corren sobre el esquema que crea Flyway,
así que los triggers y las restricciones también actúan en ellas.

Si tocas el esquema y el workflow se pone rojo, lo más probable es que haya que
actualizar `seed.sql` o alguno de los conteos del workflow.

## Qué falta

Está listado al final de `schema.sql` y de `modelo-logico.md`: reglas de negocio
Está listado al final de `V1__esquema_inicial.sql` y de `modelo-logico.md`: reglas de negocio
que el esquema no puede garantizar de forma declarativa y siguen viviendo en la
aplicación. Conviene leerlo antes de dar algo por cubierto.
4 changes: 2 additions & 2 deletions docs/database/consultas-clave.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Consultas clave — Plataforma de Reservas

Preguntas de negocio del caso traducidas a SQL. Todas se ejecutan sobre las
tablas que crea `schema.sql` y todas devuelven filas con los datos de
tablas que crean las migraciones de Flyway y todas devuelven filas con los datos de
`seed.sql`. La versión ejecutable de corrido, con separadores, está en
`consultas-clave.sql`; el workflow `database.yml` la corre en cada pull request
y falla si alguna deja de devolver resultado.
Expand Down Expand Up @@ -234,6 +234,6 @@ where l.status = 'ACTIVE'
order by o.name, l.name;
```

Ver `schema.sql` (modelo físico), `modelo-logico.md` (entidades y
Ver las migraciones de `src/main/resources/db/migration/` (modelo físico), `modelo-logico.md` (entidades y
normalización), `seed.sql` (los datos sobre los que corren estas consultas) y
`consultas-clave.sql` (la versión ejecutable).
Loading
Loading