-- +goose Up -- Hard guarantee against double-booking. The service layer checks for -- collisions before inserting, but check-then-insert is racy under concurrent -- requests; these exclusion constraints enforce the same rules atomically. CREATE EXTENSION IF NOT EXISTS btree_gist; -- Two appointments may not overlap for the same staff member. ALTER TABLE bookings ADD CONSTRAINT bookings_staff_no_overlap EXCLUDE USING gist ( tenant_id WITH =, staff_id WITH =, tstzrange(starts_at, ends_at) WITH && ) WHERE (booking_mode = 'appointment' AND status <> 'cancelled' AND staff_id IS NOT NULL); -- Two appointments may not overlap in the same location. ALTER TABLE bookings ADD CONSTRAINT bookings_location_no_overlap EXCLUDE USING gist ( tenant_id WITH =, location_id WITH =, tstzrange(starts_at, ends_at) WITH && ) WHERE (booking_mode = 'appointment' AND status <> 'cancelled' AND location_id IS NOT NULL); -- Appointments with no staff or location assignment occupy the tenant's -- single implicit resource (mirrors resourcesCollide in the service layer). ALTER TABLE bookings ADD CONSTRAINT bookings_unassigned_no_overlap EXCLUDE USING gist ( tenant_id WITH =, tstzrange(starts_at, ends_at) WITH && ) WHERE (booking_mode = 'appointment' AND status <> 'cancelled' AND staff_id IS NULL AND location_id IS NULL); -- +goose Down ALTER TABLE bookings DROP CONSTRAINT IF EXISTS bookings_unassigned_no_overlap; ALTER TABLE bookings DROP CONSTRAINT IF EXISTS bookings_location_no_overlap; ALTER TABLE bookings DROP CONSTRAINT IF EXISTS bookings_staff_no_overlap;