Introducción
Un grupo quiere asistir junto. Reservar una plaza de una solicitud para dos personas no es un éxito. Guardarás una instrucción SQL reutilizable que devuelva una confirmación real para todo el grupo o no haga ningún cambio cuando no haya espacio suficiente. Un fallo real de clave única pondrá a prueba si la asignación de plazas y la confirmación pertenecen a la misma transacción.
Este laboratorio independiente prepara talleres nuevos y reservas originales; no usa archivos del laboratorio anterior. Necesitas sus conceptos de transacción, bloqueo de fila y ROLLBACK, además de SELECT, INSERT y UPDATE básicos de PostgreSQL. Aquí se explican los parámetros de cliente con nombre, las conversiones a entero y una expresión de tabla común que modifica datos. Workshop Desk es un intérprete del sistema; Desk A y Desk B son conexiones psql independientes a la misma base de datos en una sola máquina. La duración estimada por el autor incluye lectura y errores habituales, sin un estudio de tiempos de principiantes.
Crea /home/labex/project/workshop-desk/reserve.sql. Conserva las herramientas proporcionadas, las definiciones de tablas, las reservas originales y el ID, título, aforo y nota de cada taller. Tu archivo contiene una instrucción SQL y ningún comando psql, comando del intérprete, BEGIN/COMMIT explícito, cambio de configuración ni recurso externo. Recibe los cuatro parámetros documentados y devuelve los cuatro campos de la reserva real. Se acepta SQL seguro equivalente. Este es un ejercicio controlado con READ COMMITTED, sin pagos, reintentos automáticos ni garantías distribuidas de admisión.
Guardar una asignación de plazas que alimente una confirmación
En este paso, guardarás una instrucción que no pueda crear una confirmación a menos que haya obtenido realmente las plazas del grupo.
Empieza en Workshop Desk. ./ ejecuta un programa preparado del directorio actual. El programa de estado lee tablas reales sin completar trabajo ausente:
./desk status
El taller 201 tiene un aforo de 6, una plaza reservada original y 5 plazas libres. El taller 202 tiene 3 plazas y el 203 tiene 4. Debe conservarse original-family|201|1|Original. La primera presentación usa workshop_id|title|capacity|places_left|booked_seats; la segunda usa request_code|workshop_id|seats|desk.
Tu archivo aceptará estas variables de psql: workshop_id, seats, request_code y desk. Son parámetros de cliente proporcionados por separado para cada solicitud, no columnas de base de datos ni ajustes permanentes del servidor. La forma :'name' pide a psql que inserte esa variable como un literal de cadena SQL entrecomillado de forma segura; conserva un apóstrofo dentro de un código de solicitud. No la reemplaces por un :name sin comillas. ::integer de PostgreSQL convierte el texto del taller y del tamaño positivo de grupo en números enteros.
WITH claimed AS (...) da nombre a una expresión de tabla común, o CTE: una relación intermedia usada por esta instrucción. Aquí la CTE realiza un UPDATE condicional. Su RETURNING workshop_id proporciona una fila solo cuando cabe todo el grupo solicitado. INSERT ... SELECT ... FROM claimed consume esa fila real devuelta. Por ello, una asignación vacía no produce INSERT. El RETURNING final informa de la reserva realmente insertada. No vuelvas a leer la tabla base esperando que los nuevos valores de una CTE que modifica datos sean visibles allí dentro de la misma instrucción; usa su relación RETURNING.
La resta usa el tamaño solicitado y la condición WHERE comprueba ese mismo tamaño. Una instrucción se ejecuta en una transacción implícita: o tienen éxito tanto la asignación de plazas como el INSERT, o un error SQL revierte ambos. En la prueba posterior de dos puestos, una transacción explícita mantendrá esa misma instrucción sin confirmar para que puedas observar su bloqueo.
Guarda la instrucción completa. cat > reserve.sql reemplaza ese archivo por el texto siguiente. El marcador SQL entre comillas del here-document impide que el intérprete cambie su contenido. Pega el SQL de cierre solo en su línea. Ejecuta esto en el intérprete Workshop Desk, no en ninguno de los puestos de base de datos:
cat > reserve.sql <<'SQL'
WITH claimed AS (
UPDATE workshops
SET places_left = places_left - :'seats'::integer
WHERE workshop_id = :'workshop_id'::integer
AND places_left >= :'seats'::integer
AND :'seats'::integer > 0
RETURNING workshop_id
)
INSERT INTO bookings (request_code, workshop_id, seats, desk)
SELECT :'request_code', workshop_id, :'seats'::integer, :'desk'
FROM claimed
RETURNING request_code, workshop_id, seats, desk;
SQL
Lee el archivo guardado antes de usarlo. cat lo muestra; un error de escritura debe corregirse en la fuente guardada, no ocultarse en un comando aparte:
cat reserve.sql
desk book ejecuta este archivo exacto usando parámetros nativos de psql; no proporciona una consulta de referencia ni crea reservas alternativas ni repara saldos. Sus argumentos son el ID de taller, el tamaño positivo del grupo, el código único de solicitud y la etiqueta de puesto. Ejecuta una solicitud normal de dos personas:
./desk book 201 2 normal-group A
La línea real devuelta es:
normal-group|201|2|A
Inspecciona la fuente de verdad:
./desk status
El taller 201 tiene ahora 3 plazas libres y 3 reservadas. La familia original permanece junto a tu reserva exacta de dos personas. Una línea devuelta por sí sola no demuestra un algoritmo correcto: la comprobación también lee filas reales, conserva los originales y llama a tu fuente sin cambios con distintos datos aislados, incluidas conexiones competidoras reales. Esas bases de datos temporales de prueba nunca reparan este puesto real.
Si el SQL falla, el programa imprime el error nativo y termina con un estado distinto de cero; no lo llama falta de plazas. En una transacción explícita interactiva, usa primero ROLLBACK en ese mismo puesto. Un error de escritura que no se haya confirmado no cambia nada. Si confirmaste por accidente una reserva de prueba nueva errónea, elimina solo su código exacto y recalcula únicamente el saldo de su taller en una transacción, como se enseñó en el laboratorio anterior. Por ejemplo, en Desk A, esta recuperación segura cuando no hay nada que cambiar elimina solo un typo-trial hipotético en 201:
BEGIN;
DELETE FROM bookings WHERE request_code = 'typo-trial' AND workshop_id = 201;
UPDATE workshops
SET places_left = capacity -
(SELECT COALESCE(SUM(seats), 0) FROM bookings WHERE workshop_id = 201)
WHERE workshop_id = 201;
COMMIT;
Nunca elimines original-family ni una reserva solicitada legítimamente. Inspecciona el estado antes de decidir qué trabajo queda. No repitas una solicitud ya realizada con éxito solo porque un comando posterior haya fallado.
Probar grupos en espera y una confirmación fallida
En este paso, ejecutarás el mismo archivo guardado en dos conexiones nativas independientes, mantendrás otro taller en marcha y demostrarás que un INSERT fallido no puede consumir plazas.
Cambia a Desk A. Su indicador significa que estás dentro de psql, no del intérprete del sistema. El SQL termina con punto y coma. Los comandos psql que empiezan por una barra invertida controlan el cliente y no llevan punto y coma. \set establece una variable de cliente en esta conexión; no se transmite a otra pestaña. \i lee y ejecuta el archivo indicado. El nombre de archivo absoluto funciona independientemente del directorio actual del cliente.
Inicia una transacción READ COMMITTED y envía un grupo de dos al taller 202, que inicialmente tiene tres plazas:
BEGIN ISOLATION LEVEL READ COMMITTED;
\set workshop_id 202
\set seats 2
\set request_code race-A
\set desk A
\i /home/labex/project/workshop-desk/reserve.sql
A ve race-A | 202 | 2 | A e INSERT 0 1: la instrucción insertó una fila de reserva que representa dos plazas. Todavía no se ha confirmado. El * del indicador marca la transacción abierta.
Cambia a Desk B y prepara su solicitud independiente:
BEGIN ISOLATION LEVEL READ COMMITTED;
\set workshop_id 202
\set seats 2
\set request_code race-B
\set desk B
\i /home/labex/project/workshop-desk/reserve.sql
B espera el bloqueo de fila de A, sin devolver un indicador. Déjalo esperando y cambia a Workshop Desk:
./desk waits
El wait_event_type de Desk B es Lock, con el ID real de proceso servidor de A en blocked_by. Con READ COMMITTED, después de que A confirme, el UPDATE en espera volverá a comprobar que haya plazas suficientes sobre la fila actualizada. El saldo visible anterior no es una reserva.
Todavía en Workshop Desk, usa el mismo archivo guardado para reservar otro taller:
./desk book 203 2 other-workshop C
Devuelve other-workshop|203|2|C y termina mientras A sigue bloqueando 202. No introduzcas un único bloqueo global que detenga innecesariamente este taller ajeno.
Cambia a Desk A:
COMMIT;
Cambia a Desk B. Su archivo pendiente termina ahora con cero filas devueltas e INSERT 0 0. Solo queda una plaza, por lo que este grupo de dos personas no cabe. No dividas el grupo ni insertes una confirmación aparte. Termina la transacción de B:
ROLLBACK;
Cambia a Workshop Desk:
./desk status
El taller 202 tiene una plaza libre y exactamente race-A|202|2|A; no existe race-B. El taller 203 tiene dos plazas libres y su reserva de dos personas. Comprueba la relación, no solo un código de salida de éxito.
A continuación, haz que el INSERT falle mientras todavía hay espacio suficiente para obtener una plaza en 202. Una clave primaria exige que cada código de solicitud sea único en toda la tabla de reservas, no solo dentro de un taller. El código original-family ya pertenece a 201. Reutilizarlo aquí debería producir SQLSTATE 23505, una violación de clave única. Es un fallo previsto; no es un resultado sin disponibilidad:
./desk book 202 1 original-family A
La ejecución del archivo informa de su error nativo y termina con estado 3. Captura ese estado inmediatamente, antes de que otro comando del intérprete reemplace $? (el estado de salida del comando anterior):
printf 'Previous native exit: %s\n' "$?"
Deberías ver Previous native exit: 3. Un estado distinto de cero distingue el error de un resultado correcto sin filas. La transacción implícita de la instrucción debe deshacer la asignación de plazas anterior. Lee de nuevo el registro:
./desk status
El taller 202 sigue teniendo una plaza y original-family no ha cambiado en 201. Un atajo como ON CONFLICT DO NOTHING después de la asignación puede ocultar el fallo de INSERT mientras confirma la reducción de plazas. Eso no es un rechazo atómico: el contador dejaría de coincidir con las reservas reales. El comprobador prueba este caso con espacio suficiente, para que una asignación vacía no haga superar por accidente la prueba de error.
Usa un código de solicitud nuevo para reservar legítimamente esa última plaza:
./desk book 202 1 after-error A
Deberías ver after-error|202|1|A. Ahora solicita otro grupo de dos cuando no quedan plazas:
./desk book 202 2 too-late B
El programa imprime No booking returned. Inspect the real ledger before drawing a conclusion. Esta llamada termina con cero porque el SQL era válido y no devolvió ninguna reserva; no debe dejar ninguna fila too-late ni cambiar ningún saldo.
./desk status
Las nuevas reservas finales son normal-group (201, 2, A), race-A (202, 2, A), other-workshop (203, 2, C) y after-error (202, 1, A), además de la familia original intacta. No existe race-B ni too-late. Las plazas restantes son 3, 0 y 2 respectivamente.
Si cancelas un archivo en espera con Ctrl+C, SQLSTATE 57014 aborta su transacción explícita. Como practicaste antes, usa ROLLBACK en esa misma pestaña de base de datos antes de reintentar. Inspecciona primero el registro confirmado: el otro puesto puede haber tenido éxito ya. Un error SQL interactivo y un resultado normal de cero filas requieren decisiones diferentes.
Resumen
Tu SQL guardado convierte una asignación condicional real de plazas en exactamente una confirmación de grupo completo. Dos conexiones nativas demostraron la espera y la reevaluación mientras otro taller continuaba. Un código de solicitud duplicado provocó un fallo real de INSERT y revirtió su asignación de plazas; una solicitud legítima posterior usó después la plaza que seguía disponible. El archivo reutilizable y el registro real conservan los datos originales y distinguen una falta de plazas de un error SQL.



