Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
109 changes: 109 additions & 0 deletions .github/workflows/database.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
name: database

# Comprobación automática del modelo de datos.
# El caso exige "integrar mediante pull request, revisión por pares y
# comprobaciones automáticas obligatorias". Este workflow cubre la parte
# de base de datos: levanta un PostgreSQL vacío y verifica que el DDL
# ejecute, que los datos de prueba carguen, que las consultas clave
# devuelvan filas y que las restricciones rechacen lo que deben rechazar.
#
# 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).

on:
push:
branches: [main]
paths:
- 'docs/database/**'
- 'src/main/java/**/persistence/**'
- '.github/workflows/database.yml'
pull_request:
paths:
- 'docs/database/**'
- 'src/main/java/**/persistence/**'
- '.github/workflows/database.yml'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: database-${{ github.ref }}
cancel-in-progress: true

jobs:
esquema:
name: Esquema, datos y restricciones
runs-on: ubuntu-latest
timeout-minutes: 10

services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: reservas
ports: ['5432:5432']
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10

env:
PGPASSWORD: postgres
PSQL: psql -h localhost -U postgres -d reservas

steps:
- uses: actions/checkout@v4

- name: Comprobar el cliente de PostgreSQL
run: |
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: 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

- name: Los datos de prueba cargan
run: $PSQL -v ON_ERROR_STOP=1 -q -f docs/database/seed.sql

- name: Las catorce consultas clave devuelven filas
run: |
salida=$($PSQL -v ON_ERROR_STOP=1 -f docs/database/consultas-clave.sql)
echo "$salida"
# Sólo cuentan los bloques con al menos una fila: "(0 rows)" no vale.
con_filas=$(echo "$salida" | grep -cE '^\([1-9][0-9]* row' || true)
vacias=$(echo "$salida" | grep -cE '^\(0 rows\)' || true)
echo "consultas con filas: $con_filas · vacías: $vacias"
test "$con_filas" -eq 14
test "$vacias" -eq 0

- name: Las restricciones rechazan lo que deben rechazar
run: |
salida=$($PSQL -f docs/database/pruebas-integridad.sql 2>&1)
echo "$salida"
# psql -f antepone "archivo:linea:" a cada error, así que el
# patrón no puede anclarse sólo al principio de la línea.
rechazos=$(echo "$salida" | grep -cE '(^|: )ERROR:' || true)
coladas=$(echo "$salida" | grep -cE '^(INSERT|UPDATE|DELETE) ' || true)
echo "rechazos: $rechazos · instrucciones que pasaron: $coladas"
test "$rechazos" -eq 18
test "$coladas" -eq 0

- 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
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,16 @@ contraseña). MFA de administradores diferido a Sprint 2.
| ADR-0002 Supabase (BD + identidad) | `docs/adr/ADR-0002-*.md` |
| 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` |
| 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 inicial (DDL) | `docs/database/schema.sql` |
| Modelo físico (DDL, 19 tablas) | `docs/database/schema.sql` |
| 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` |
| 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` |
| Runbook operativo (producción, E2E, troubleshooting) | `docs/operations/runbook.md` |
| Colección REST de Sprint 1 | `docs/api/sprint1.http` |

Expand Down Expand Up @@ -137,8 +143,10 @@ Linux/macOS:

Notas:

- El primer arranque crea solo las tablas (`ddl-auto=update`). Después
ejecuta `docs/database/rls.sql` en el SQL Editor de Supabase.
- 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`.
- Swagger UI: http://localhost:8080/swagger-ui.html
- Health: http://localhost:8080/actuator/health

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
- **Estado:** Aceptado; la estrategia de esquema la modifica el ADR-0005
- **Fecha:** 2026-09-18
- **Prioridad:** Alta
- **Decisores:** Equipo de desarrollo — rol Arquitecto de Software y BD
Expand Down
81 changes: 81 additions & 0 deletions docs/adr/ADR-0005-modelo-fisico-escrito-a-mano.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# 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
- **Fecha:** 2026-09-21
- **Prioridad:** Alta
- **Decisores:** Equipo de desarrollo — rol Arquitecto de Software y BD

## Contexto

El ADR-0002 dejó `hibernate.ddl-auto=update` en desarrollo y `validate` en la
nube, con el modelo físico "versionado como script exportado en
`docs/database/schema.sql`". Esa estrategia cumplió para arrancar el Sprint 1,
pero produjo tres huecos: el script exportado documenta las dos tablas que hoy
tienen entidad JPA (`clients`, `login_attempts`) mientras `modelo-logico.md`
describe dieciocho; un script generado desde el mapeo nunca lleva claves
foráneas, índices de consulta ni restricciones de negocio; y las consultas de
`consultas-clave.md` no se pueden ejecutar porque las tablas que referencian no
existen en ningún lado. El caso exige además "comprobaciones automáticas
obligatorias" en los pull requests, y el repositorio no tenía ninguna.

## Decisión

Invertir la relación entre el código y el esquema: **`docs/database/schema.sql`
pasa a ser la fuente del modelo de datos**, escrita a mano, y deja de ser un
volcado de lo que Hibernate genera.

1. **Alcance completo del caso:** diecinueve tablas que cubren identidad,
organización, catálogo de servicios, recursos, agenda y reservas. Las dos
tablas ya implementadas se reproducen con los mismos nombres, tipos y largos
que produce el mapeo actual, incluido el `timestamp(6)` sin zona de
`AuditableEntity`, para no romper el `validate` del perfil `cloud`.
2. **Restricciones de negocio en la base:** las reglas de HU-001 a HU-004 que
se pueden expresar de forma declarativa viven en el esquema. Entre ellas, la
restricción `exclude using gist` que impide que un recurso quede asignado a
dos sesiones solapadas, que es el problema de sobreocupación del enunciado.
3. **Verificación automática:** el workflow `.github/workflows/database.yml`
levanta un PostgreSQL vacío y comprueba que el DDL ejecute, que sea
idempotente, que los datos de prueba carguen, que las catorce consultas
clave devuelvan filas y que las dieciocho violaciones de
`pruebas-integridad.sql` sean rechazadas. Se dispara tanto con cambios en
`docs/database/` como en el mapeo JPA, que es donde puede nacer la
divergencia entre el esquema y las entidades.

El script es idempotente (`if not exists` en todas las sentencias `create`), de
modo que se puede aplicar sobre la base actual de Supabase sin alterar las dos
tablas existentes. La política de **RLS activado en todas las tablas de
negocio** del ADR-0002 se extiende a las diecinueve en `docs/database/rls.sql`.

## Alternativas consideradas

1. **Mantener el script como export de `ddl-auto`:** cuesta cero esfuerzo y
sigue el flujo actual, pero el esquema solo puede crecer al ritmo al que se
escriben entidades JPA, y el entregable de datos del sprint quedaría sin
claves foráneas ni consultas ejecutables.
2. **Adoptar Flyway ya en el Sprint 1:** es el destino correcto y el ADR-0002
lo dejó como deuda priorizada, pero exige decidir la línea base, reescribir
el arranque local y coordinarlo con el despliegue; se mantiene para el
Sprint 2, y este ADR le prepara el terreno al dejar un esquema explícito que
puede convertirse en la migración `V1__baseline.sql`.

## Consecuencias

- (+) El modelo de datos del caso queda completo y ejecutable, no solo dibujado.
- (+) Las reglas de negocio quedan garantizadas por el motor y no dependen de
que cada caso de uso se acuerde de validarlas.
- (+) El repositorio gana su primera comprobación automática, que es un
lineamiento explícito del caso.
- (−) Aparece la posibilidad de que el esquema escrito a mano y el mapeo JPA
diverjan; se compensa con `ddl-auto=validate` en la nube, que falla al
arrancar si una entidad no cuadra con su tabla.
- (−) Diecisiete de las diecinueve tablas todavía no tienen entidad JPA: son
modelo de datos adelantado al código. Mientras no se implementen, `validate`
las ignora porque solo comprueba las tablas mapeadas.
- (−) `seed.sql` y `pruebas-integridad.sql` usan datos ficticios acoplados a
identificadores fijos; si el esquema cambia hay que mantenerlos, y el
workflow avisa cuando se rompen.

Ver `docs/database/schema.sql` (modelo físico), `modelo-logico.md` (entidades y
normalización), `consultas-clave.md` (preguntas de negocio), `seed.sql` (datos
de prueba), `pruebas-integridad.sql` (restricciones en acción) y `rls.sql`
(seguridad Supabase).
71 changes: 71 additions & 0 deletions docs/database/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Base de datos — guía de la carpeta

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

## Los archivos

| 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 |
| `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 |
| `der-0-modelo-completo.png` y `der-1..4-*.png` | Las cinco páginas del `.drawio` exportadas | No |
| `er-identidad.png`, `er-catalogo.png`, `er-reservas.png` | Los diagramas de `modelo-logico.md` exportados, por si Mermaid no carga | No |

Los dos `.sql` de consultas usan metacomandos de `psql` (`\echo`), así que se
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:

```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
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
```

Y para mirar los datos:

```bash
docker compose exec db psql -U postgres -d postgres
```

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

`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).

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.

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
que el esquema no puede garantizar de forma declarativa y siguen viviendo en la
aplicación. Conviene leerlo antes de dar algo por cubierto.
Loading
Loading