feat(db): add computed booking fields with triggers and indexes
Add total_duration_minutes, total_amount, end_time computed columns to bookings table with PL/pgSQL triggers for auto-recalculation. Add indexes for new columns (end_time, status+end_time, notes trigram) and existing join columns (service_id, custom_service_id). Add trigram indexes for user name and service name search. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -247,6 +247,9 @@ CREATE INDEX idx_users_account_role ON users (account_role);
|
||||
CREATE INDEX idx_users_fn_trgm ON users USING GIN (fn gin_trgm_ops);
|
||||
CREATE INDEX idx_users_email_trgm ON users USING GIN (email gin_trgm_ops);
|
||||
CREATE INDEX idx_users_phone_trgm ON users USING GIN (phone gin_trgm_ops);
|
||||
CREATE INDEX idx_users_created_at ON users(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_n_first_name_trgm ON users USING GIN (n_first_name gin_trgm_ops);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_n_last_name_trgm ON users USING GIN (n_last_name gin_trgm_ops);
|
||||
|
||||
-- Enforce unique email only for registered (non-guest) users
|
||||
-- Guest accounts can share emails; registered accounts cannot
|
||||
@@ -270,6 +273,7 @@ CREATE TABLE name_history (
|
||||
|
||||
CREATE INDEX idx_name_history_user_id ON name_history(user_id);
|
||||
CREATE INDEX idx_name_history_changed_at ON name_history(changed_at);
|
||||
CREATE INDEX idx_name_history_user_active ON name_history(user_id) WHERE booking_id IS NULL;
|
||||
|
||||
-- =======================================
|
||||
-- VERIFICATION CODES TABLE
|
||||
@@ -337,6 +341,7 @@ CREATE TABLE services (
|
||||
);
|
||||
|
||||
CREATE INDEX idx_services_name ON services(name);
|
||||
CREATE INDEX IF NOT EXISTS idx_services_name_trgm ON services USING GIN (name gin_trgm_ops);
|
||||
|
||||
-- =======================================
|
||||
-- CUSTOM SERVICES TABLE (One-off / special request services)
|
||||
@@ -372,6 +377,10 @@ CREATE TABLE bookings (
|
||||
status booking_status NOT NULL DEFAULT 'pending',
|
||||
notes TEXT,
|
||||
deposit_required BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
-- Computed fields (maintained by trigger on booking_services/booking_custom_services)
|
||||
total_duration_minutes INT NOT NULL DEFAULT 60,
|
||||
total_amount NUMERIC(10,2) NOT NULL DEFAULT 0,
|
||||
end_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Set by BEFORE INSERT trigger; recalculated by booking_services trigger
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by CHAR(12),
|
||||
@@ -379,9 +388,10 @@ CREATE TABLE bookings (
|
||||
);
|
||||
|
||||
CREATE INDEX idx_bookings_userid ON bookings(user_id);
|
||||
CREATE INDEX idx_bookings_starttime ON bookings(start_time);
|
||||
CREATE INDEX idx_bookings_status ON bookings(status);
|
||||
CREATE INDEX idx_bookings_userid_starttime ON bookings(user_id, start_time);
|
||||
CREATE INDEX idx_bookings_end_time ON bookings(end_time);
|
||||
CREATE INDEX idx_bookings_status_end_time ON bookings(status, end_time);
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_notes_trgm ON bookings USING GIN (notes gin_trgm_ops);
|
||||
|
||||
ALTER TABLE name_history ADD CONSTRAINT fk_name_history_booking_id
|
||||
FOREIGN KEY (booking_id) REFERENCES bookings(id) ON DELETE SET NULL;
|
||||
@@ -398,6 +408,8 @@ CREATE TABLE booking_services (
|
||||
PRIMARY KEY (booking_id, service_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_booking_services_service_id ON booking_services(service_id);
|
||||
|
||||
-- =======================================
|
||||
-- BOOKING CUSTOM SERVICES JUNCTION TABLE
|
||||
-- =======================================
|
||||
@@ -410,6 +422,69 @@ CREATE TABLE booking_custom_services (
|
||||
PRIMARY KEY (booking_id, custom_service_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_booking_custom_services_custom_service_id ON booking_custom_services(custom_service_id);
|
||||
|
||||
-- =======================================
|
||||
-- TRIGGER: Auto-recalculate booking duration & total
|
||||
-- =======================================
|
||||
-- WHY: Keeps bookings.total_duration_minutes, bookings.total_amount,
|
||||
-- and the generated bookings.end_time in sync when services change.
|
||||
|
||||
CREATE OR REPLACE FUNCTION recalc_booking_duration_and_total()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
bid CHAR(12);
|
||||
BEGIN
|
||||
bid := COALESCE(NEW.booking_id, OLD.booking_id);
|
||||
UPDATE bookings b SET
|
||||
total_duration_minutes = sub.dur,
|
||||
total_amount = sub.amt,
|
||||
end_time = b.start_time + (sub.dur * INTERVAL '1 minute')
|
||||
FROM (
|
||||
SELECT
|
||||
COALESCE((SELECT SUM(dur) FROM (
|
||||
SELECT COALESCE(bs.override_duration_minutes, s.duration_minutes) AS dur
|
||||
FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = bid
|
||||
UNION ALL
|
||||
SELECT COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
|
||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = bid
|
||||
) sub2), 60) AS dur,
|
||||
COALESCE((SELECT SUM(amt) FROM (
|
||||
SELECT COALESCE(bs.override_price, s.price) AS amt
|
||||
FROM booking_services bs JOIN services s ON bs.service_id = s.id WHERE bs.booking_id = bid
|
||||
UNION ALL
|
||||
SELECT COALESCE(bcs.override_price, cs.price)
|
||||
FROM booking_custom_services bcs JOIN custom_services cs ON bcs.custom_service_id = cs.id WHERE bcs.booking_id = bid
|
||||
) sub2), 0) AS amt
|
||||
) sub
|
||||
WHERE b.id = bid;
|
||||
RETURN COALESCE(NEW, OLD);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_recalc_booking_duration_total
|
||||
AFTER INSERT OR UPDATE OR DELETE ON booking_services
|
||||
FOR EACH ROW EXECUTE FUNCTION recalc_booking_duration_and_total();
|
||||
|
||||
CREATE TRIGGER trg_recalc_booking_duration_total_custom
|
||||
AFTER INSERT OR UPDATE OR DELETE ON booking_custom_services
|
||||
FOR EACH ROW EXECUTE FUNCTION recalc_booking_duration_and_total();
|
||||
|
||||
-- BEFORE INSERT trigger on bookings to set end_time from defaults
|
||||
-- (the AFTER trigger on booking_services overrides this when services are added)
|
||||
CREATE OR REPLACE FUNCTION set_booking_end_time()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.end_time := NEW.start_time + (NEW.total_duration_minutes * INTERVAL '1 minute');
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_set_booking_end_time
|
||||
BEFORE INSERT ON bookings
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_booking_end_time();
|
||||
|
||||
-- =======================================
|
||||
-- BOOKING EDIT REQUESTS TABLE
|
||||
-- =======================================
|
||||
@@ -431,6 +506,7 @@ CREATE TABLE booking_edit_requests (
|
||||
);
|
||||
|
||||
CREATE INDEX idx_booking_edit_requests_booking ON booking_edit_requests(booking_id);
|
||||
CREATE INDEX idx_booking_edit_requests_requested_by ON booking_edit_requests(requested_by);
|
||||
|
||||
CREATE TABLE user_referrals (
|
||||
id CHAR(12) PRIMARY KEY DEFAULT generate_user_referrals_id(),
|
||||
@@ -465,8 +541,7 @@ CREATE TABLE working_hours (
|
||||
is_open BOOLEAN NOT NULL DEFAULT TRUE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_working_hours_weekday ON working_hours(weekday);
|
||||
|
||||
-- weekday is already indexed via PRIMARY KEY — no separate index needed
|
||||
INSERT INTO working_hours VALUES (0, '00:00:00', '00:00:00', FALSE);
|
||||
INSERT INTO working_hours VALUES (1, '09:00:00', '17:00:00', TRUE);
|
||||
INSERT INTO working_hours VALUES (2, '09:00:00', '17:00:00', TRUE);
|
||||
@@ -526,6 +601,9 @@ CREATE TABLE time_blockers (
|
||||
-- Indexes for efficient overlap queries
|
||||
CREATE INDEX idx_time_blockers_start_time ON time_blockers(start_time);
|
||||
CREATE INDEX idx_time_blockers_cron ON time_blockers(cron_expression) WHERE cron_expression IS NOT NULL;
|
||||
CREATE INDEX idx_time_blockers_description ON time_blockers(description);
|
||||
CREATE INDEX idx_time_blockers_created_by ON time_blockers(created_by);
|
||||
CREATE INDEX IF NOT EXISTS idx_time_blockers_desc_trgm ON time_blockers USING GIN (description gin_trgm_ops);
|
||||
|
||||
-- =======================================
|
||||
-- FORGIVEN NO-SHOWS TABLE
|
||||
@@ -583,6 +661,8 @@ CREATE INDEX idx_payments_status ON payments(status);
|
||||
CREATE INDEX idx_payments_createdat ON payments(created_at);
|
||||
CREATE INDEX idx_payments_booking_id_status ON payments(booking_id, status);
|
||||
CREATE INDEX idx_payments_created_at_status ON payments(created_at, status);
|
||||
CREATE INDEX idx_payments_payment_method ON payments(payment_method);
|
||||
CREATE INDEX idx_payments_payment_method_created_at ON payments(payment_method, created_at);
|
||||
|
||||
-- =======================================
|
||||
-- LOYALTY REDEMPTIONS TABLE
|
||||
@@ -652,6 +732,7 @@ CREATE INDEX idx_booking_discounts_booking ON booking_discounts(booking_id);
|
||||
CREATE INDEX idx_booking_discounts_source ON booking_discounts(discount_source, source_id);
|
||||
CREATE INDEX idx_booking_discounts_user ON booking_discounts(user_id);
|
||||
CREATE INDEX idx_booking_discounts_milestone ON booking_discounts(user_id, milestone_type, source_id);
|
||||
CREATE INDEX idx_booking_discounts_booking_source ON booking_discounts(booking_id, discount_source);
|
||||
|
||||
-- =======================================
|
||||
-- BUSINESS SETTINGS TABLE (FOR COMPLIANCE)
|
||||
@@ -727,6 +808,9 @@ CREATE TABLE admin_notifications (
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_admin_notifications_reason ON admin_notifications(reason);
|
||||
CREATE INDEX idx_admin_notifications_acknowledged_at ON admin_notifications(acknowledged_at);
|
||||
|
||||
-- User notification preferences for future user notification system
|
||||
CREATE TABLE user_notification_preferences (
|
||||
id CHAR(12) PRIMARY KEY DEFAULT generate_user_notification_preferences_id(),
|
||||
@@ -739,9 +823,6 @@ CREATE TABLE user_notification_preferences (
|
||||
|
||||
CREATE INDEX idx_user_notification_preferences_user_id ON user_notification_preferences(user_id);
|
||||
|
||||
CREATE INDEX idx_bookings_start_time_status ON bookings(start_time, status);
|
||||
CREATE INDEX idx_users_created_at ON users(created_at);
|
||||
|
||||
create table images (
|
||||
id CHAR(12) PRIMARY KEY DEFAULT generate_images_id(),
|
||||
url text not null,
|
||||
@@ -769,6 +850,8 @@ create index idx_images_tag_names on images using gin(tag_names);
|
||||
-- For ILIKE search on unnested tags at scale, normalize into a separate image_tags table
|
||||
-- and add: CREATE INDEX idx_image_tags_name_trgm ON image_tags USING GIN (name gin_trgm_ops);
|
||||
create index idx_images_created_at on images(created_at desc);
|
||||
CREATE INDEX IF NOT EXISTS idx_images_url_trgm ON images USING GIN (url gin_trgm_ops);
|
||||
CREATE INDEX IF NOT EXISTS idx_images_thumbnail_url_trgm ON images USING GIN (thumbnail_url gin_trgm_ops);
|
||||
create index idx_tags_name_trgm on tags using gin (name gin_trgm_ops);
|
||||
|
||||
-- =======================================
|
||||
@@ -2169,30 +2252,6 @@ INSERT INTO dav_calendars (principaluri, displayname, uri, description, componen
|
||||
SELECT 'principals/default', 'Default Calendar', 'default', 'Default calendar', 'VEVENT,VTODO', false
|
||||
WHERE NOT EXISTS (SELECT 1 FROM dav_calendars WHERE uri = 'default');
|
||||
|
||||
-- =======================================
|
||||
-- PG_TRGM GIN INDEXES FOR ILIKE SEARCH WITH LEADING WILDCARDS
|
||||
-- These indexes enable efficient ILIKE search patterns like '%search%'
|
||||
-- by using trigram matching via the pg_trgm extension (already enabled).
|
||||
-- =======================================
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_bookings_notes_trgm ON bookings USING GIN (notes gin_trgm_ops);
|
||||
|
||||
-- users: n_first_name and n_last_name for admin user search
|
||||
CREATE INDEX IF NOT EXISTS idx_users_n_first_name_trgm ON users USING GIN (n_first_name gin_trgm_ops);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_n_last_name_trgm ON users USING GIN (n_last_name gin_trgm_ops);
|
||||
|
||||
-- services: name for booking search (ILIKE with leading wildcard; B-tree idx_services_name is useless for that)
|
||||
CREATE INDEX IF NOT EXISTS idx_services_name_trgm ON services USING GIN (name gin_trgm_ops);
|
||||
|
||||
-- images: url and thumbnail_url for portfolio image lookup
|
||||
CREATE INDEX IF NOT EXISTS idx_images_url_trgm ON images USING GIN (url gin_trgm_ops);
|
||||
CREATE INDEX IF NOT EXISTS idx_images_thumbnail_url_trgm ON images USING GIN (thumbnail_url gin_trgm_ops);
|
||||
|
||||
|
||||
|
||||
-- time_blockers: description for ILIKE ANY search
|
||||
CREATE INDEX IF NOT EXISTS idx_time_blockers_desc_trgm ON time_blockers USING GIN (description gin_trgm_ops);
|
||||
|
||||
-- Account lockout audit log
|
||||
CREATE TABLE IF NOT EXISTS login_audit (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
Reference in New Issue
Block a user