| 1 | | = Phase P7 |
| 2 | | |
| 3 | | == Content |
| 4 | | To be defined. |
| | 1 | = Phase P7: Advanced Database Development (Indexes, Constraints, Triggers, Views) = |
| | 2 | |
| | 3 | == Overview == |
| | 4 | In this phase we implement advanced database mechanisms that enforce correctness and improve performance in the Wedding Planner system. |
| | 5 | Instead of relying only on application logic, we move important rules directly into PostgreSQL using indexes, constraints, triggers and views. |
| | 6 | |
| | 7 | == What we cover in this phase == |
| | 8 | * Indexes for faster searching/filtering (availability checks, RSVP reports) |
| | 9 | * Constraints for preventing invalid/inconsistent data |
| | 10 | * Triggers for automation of derived logic (pricing, capacity checks) |
| | 11 | * Views for simplified reporting (RSVP summary, confirmed bookings overview) |
| | 12 | |
| | 13 | ---- |
| | 14 | |
| | 15 | == 1) Indexes (Query Performance) == |
| | 16 | Indexes speed up database queries by avoiding full table scans. |
| | 17 | They are especially important in our system because availability checks and RSVP reports are executed frequently. |
| | 18 | |
| | 19 | === Index 1: Event date/time search === |
| | 20 | This index improves performance when searching or filtering events by date + start/end time. |
| | 21 | |
| | 22 | *Table:* `event` |
| | 23 | |
| | 24 | {{{ |
| | 25 | CREATE INDEX idx_event_date_start ON event("date", start_time); |
| | 26 | CREATE INDEX idx_event_date_end ON event("date", end_time); |
| | 27 | }}} |
| | 28 | |
| | 29 | *Why it matters:* |
| | 30 | When the system loads the schedule for a wedding day, the database can quickly find events in a time window (without scanning the whole table). |
| | 31 | |
| | 32 | === Index 2: Booking status filtering (availability checks) === |
| | 33 | Availability endpoints frequently filter bookings by date + status (only CONFIRMED bookings should block resources). |
| | 34 | |
| | 35 | *Tables:* `venue_booking`, `photographer_booking`, `band_booking` |
| | 36 | |
| | 37 | {{{ |
| | 38 | CREATE INDEX idx_venue_booking_status ON venue_booking("date", status); |
| | 39 | CREATE INDEX idx_photographer_booking_status ON photographer_booking("date", status); |
| | 40 | CREATE INDEX idx_band_booking_status ON band_booking("date", status); |
| | 41 | }}} |
| | 42 | |
| | 43 | *Reason:* |
| | 44 | These indexes make queries like “all confirmed bookings on date X” significantly faster. |
| | 45 | |
| | 46 | === Index 3: RSVP status report === |
| | 47 | RSVP summaries filter guests by status per event. |
| | 48 | |
| | 49 | *Table:* `event_rsvp` |
| | 50 | |
| | 51 | {{{ |
| | 52 | CREATE INDEX idx_rsvp_event_status ON event_rsvp (event_id, status); |
| | 53 | }}} |
| | 54 | |
| | 55 | *Reason:* |
| | 56 | This makes reporting queries efficient (e.g., confirmed vs declined guests for an event). |
| | 57 | |
| | 58 | ---- |
| | 59 | |
| | 60 | == 2) Constraints (Consistency & Integrity) == |
| | 61 | Constraints guarantee correctness even if invalid data reaches the database. |
| | 62 | In Wedding Planner, this is critical for avoiding double bookings and enforcing valid status values. |
| | 63 | |
| | 64 | === Constraint 1: Prevent overlapping bookings (NO double booking) === |
| | 65 | A venue / photographer / band must not be booked in overlapping time intervals. |
| | 66 | |
| | 67 | To support this, we first create generated timestamp columns and enable GiST indexing. |
| | 68 | |
| | 69 | {{{ |
| | 70 | CREATE EXTENSION IF NOT EXISTS btree_gist; |
| | 71 | |
| | 72 | ALTER TABLE venue_booking |
| | 73 | ADD COLUMN start_ts TIMESTAMP NOT NULL GENERATED ALWAYS AS ("date" + start_time) STORED, |
| | 74 | ADD COLUMN end_ts TIMESTAMP NOT NULL GENERATED ALWAYS AS ("date" + end_time) STORED; |
| | 75 | |
| | 76 | ALTER TABLE photographer_booking |
| | 77 | ADD COLUMN start_ts TIMESTAMP NOT NULL GENERATED ALWAYS AS ("date" + start_time) STORED, |
| | 78 | ADD COLUMN end_ts TIMESTAMP NOT NULL GENERATED ALWAYS AS ("date" + end_time) STORED; |
| | 79 | |
| | 80 | ALTER TABLE band_booking |
| | 81 | ADD COLUMN start_ts TIMESTAMP NOT NULL GENERATED ALWAYS AS ("date" + start_time) STORED, |
| | 82 | ADD COLUMN end_ts TIMESTAMP NOT NULL GENERATED ALWAYS AS ("date" + end_time) STORED; |
| | 83 | }}} |
| | 84 | |
| | 85 | Then we add EXCLUDE constraints: |
| | 86 | |
| | 87 | {{{ |
| | 88 | ALTER TABLE venue_booking |
| | 89 | ADD CONSTRAINT exclude_overlap_vb EXCLUDE USING gist ( |
| | 90 | venue_id WITH =, |
| | 91 | tsrange(start_ts, end_ts) WITH && |
| | 92 | ); |
| | 93 | |
| | 94 | ALTER TABLE photographer_booking |
| | 95 | ADD CONSTRAINT exclude_overlap_pb EXCLUDE USING gist ( |
| | 96 | photographer_id WITH =, |
| | 97 | tsrange(start_ts, end_ts) WITH && |
| | 98 | ); |
| | 99 | |
| | 100 | ALTER TABLE band_booking |
| | 101 | ADD CONSTRAINT exclude_overlap_bb EXCLUDE USING gist ( |
| | 102 | band_id WITH =, |
| | 103 | tsrange(start_ts, end_ts) WITH && |
| | 104 | ); |
| | 105 | }}} |
| | 106 | |
| | 107 | *Why this is important:* |
| | 108 | This enforces a real-world rule at database level: |
| | 109 | even if two users try to insert bookings at the same time, PostgreSQL blocks conflicts automatically. |
| | 110 | |
| | 111 | === Constraint 2: Status validation (CHECK constraints) === |
| | 112 | CHECK constraints restrict allowed values and prevent “random text statuses”. |
| | 113 | |
| | 114 | {{{ |
| | 115 | ALTER TABLE event |
| | 116 | ADD CONSTRAINT chk_event_status |
| | 117 | CHECK (status IN ('PLANNED', 'CONFIRMED', 'CANCELLED')); |
| | 118 | |
| | 119 | ALTER TABLE event_rsvp |
| | 120 | ADD CONSTRAINT chk_event_rsvp_status |
| | 121 | CHECK (status IN ('ACCEPTED', 'DECLINED', 'PENDING')); |
| | 122 | |
| | 123 | ALTER TABLE venue_booking |
| | 124 | ADD CONSTRAINT chk_venue_booking_status |
| | 125 | CHECK (status IN ('PENDING', 'CONFIRMED', 'CANCELLED')); |
| | 126 | |
| | 127 | ALTER TABLE photographer_booking |
| | 128 | ADD CONSTRAINT chk_photographer_booking_status |
| | 129 | CHECK (status IN ('PENDING', 'CONFIRMED', 'CANCELLED')); |
| | 130 | |
| | 131 | ALTER TABLE band_booking |
| | 132 | ADD CONSTRAINT chk_band_booking_status |
| | 133 | CHECK (status IN ('PENDING', 'CONFIRMED', 'CANCELLED')); |
| | 134 | }}} |
| | 135 | |
| | 136 | Additional validations: |
| | 137 | |
| | 138 | {{{ |
| | 139 | ALTER TABLE wedding |
| | 140 | ADD CONSTRAINT chk_budget CHECK (budget > 0); |
| | 141 | |
| | 142 | ALTER TABLE attendance |
| | 143 | ADD CONSTRAINT chk_att_role |
| | 144 | CHECK (role IN ('GUEST', 'BRIDE', 'GROOM', 'BEST_MAN', 'MAID_OF_HONOR')); |
| | 145 | }}} |
| | 146 | |
| | 147 | *Result:* |
| | 148 | The database rejects invalid states automatically, ensuring consistent data across the whole system. |
| | 149 | |
| | 150 | |
| | 151 | ---- |
| | 152 | |
| | 153 | == 3) Triggers (Automation of Business Logic) == |
| | 154 | Triggers automate derived logic and enforce rules without relying on the application. |
| | 155 | |
| | 156 | === Trigger 1: Automatic venue booking price calculation === |
| | 157 | The total venue booking price is derived from: |
| | 158 | `venue.price_per_guest * number_of_guests_for_wedding`. |
| | 159 | |
| | 160 | {{{ |
| | 161 | CREATE OR REPLACE FUNCTION calc_venue_price() |
| | 162 | RETURNS trigger AS $$ |
| | 163 | BEGIN |
| | 164 | IF NEW.price IS NULL THEN |
| | 165 | SELECT price_per_guest * COUNT(*) |
| | 166 | INTO NEW.price |
| | 167 | FROM venue v |
| | 168 | JOIN guest g ON g.wedding_id = NEW.wedding_id |
| | 169 | WHERE v.venue_id = NEW.venue_id; |
| | 170 | END IF; |
| | 171 | RETURN NEW; |
| | 172 | END; |
| | 173 | $$ LANGUAGE plpgsql; |
| | 174 | |
| | 175 | CREATE TRIGGER trg_calc_venue_price |
| | 176 | BEFORE INSERT ON venue_booking |
| | 177 | FOR EACH ROW |
| | 178 | EXECUTE FUNCTION calc_venue_price(); |
| | 179 | }}} |
| | 180 | |
| | 181 | *Why it matters:* |
| | 182 | This ensures consistent pricing and avoids manual calculation mistakes. |
| | 183 | |
| | 184 | === Trigger 2: Venue capacity enforcement === |
| | 185 | This trigger prevents inserting guests if venue capacity would be exceeded. |
| | 186 | |
| | 187 | {{{ |
| | 188 | CREATE OR REPLACE FUNCTION check_guest_capacity() |
| | 189 | RETURNS trigger AS $$ |
| | 190 | DECLARE |
| | 191 | guest_count INTEGER; |
| | 192 | venue_cap INTEGER; |
| | 193 | BEGIN |
| | 194 | SELECT v.capacity |
| | 195 | INTO venue_cap |
| | 196 | FROM venue_booking vb |
| | 197 | JOIN venue v ON v.venue_id = vb.venue_id |
| | 198 | WHERE vb.wedding_id = NEW.wedding_id |
| | 199 | AND vb.status = 'CONFIRMED'; |
| | 200 | |
| | 201 | IF venue_cap IS NULL THEN |
| | 202 | RETURN NEW; |
| | 203 | END IF; |
| | 204 | |
| | 205 | SELECT COUNT(*) INTO guest_count |
| | 206 | FROM guest |
| | 207 | WHERE wedding_id = NEW.wedding_id |
| | 208 | AND (TG_OP = 'INSERT' OR guest_id <> OLD.guest_id); |
| | 209 | |
| | 210 | IF guest_count + 1 > venue_cap THEN |
| | 211 | RAISE EXCEPTION 'Cannot add guest: venue capacity (%) exceeded', venue_cap; |
| | 212 | END IF; |
| | 213 | |
| | 214 | RETURN NEW; |
| | 215 | END; |
| | 216 | $$ LANGUAGE plpgsql; |
| | 217 | |
| | 218 | CREATE TRIGGER trg_guest_capacity |
| | 219 | BEFORE INSERT OR UPDATE ON guest |
| | 220 | FOR EACH ROW |
| | 221 | EXECUTE FUNCTION check_guest_capacity(); |
| | 222 | }}} |
| | 223 | |
| | 224 | *Why it matters:* |
| | 225 | It guarantees that capacity rules are enforced even if the app forgets to validate. |
| | 226 | |
| | 227 | ---- |
| | 228 | |
| | 229 | == 4) Views (Simplified Reports) == |
| | 230 | Views provide reusable report queries for the system dashboard. |
| | 231 | |
| | 232 | === View 1: RSVP summary per event === |
| | 233 | {{{ |
| | 234 | CREATE VIEW vw_event_rsvp_summary AS |
| | 235 | SELECT |
| | 236 | event_id, |
| | 237 | SUM(CASE WHEN status = 'ACCEPTED' THEN 1 ELSE 0 END) AS yes_count, |
| | 238 | SUM(CASE WHEN status = 'DECLINED' THEN 1 ELSE 0 END) AS no_count, |
| | 239 | SUM(CASE WHEN status = 'PENDING' THEN 1 ELSE 0 END) AS maybe_count |
| | 240 | FROM event_rsvp |
| | 241 | GROUP BY event_id; |
| | 242 | }}} |
| | 243 | |
| | 244 | === View 2: Confirmed bookings by date (all resources) === |
| | 245 | {{{ |
| | 246 | CREATE VIEW vw_confirmed_bookings_by_date AS |
| | 247 | SELECT |
| | 248 | "date", |
| | 249 | 'VENUE' AS booking_type, |
| | 250 | venue_id AS resource_id, |
| | 251 | start_time, |
| | 252 | end_time |
| | 253 | FROM venue_booking |
| | 254 | WHERE status = 'CONFIRMED' |
| | 255 | |
| | 256 | UNION ALL |
| | 257 | |
| | 258 | SELECT |
| | 259 | "date", |
| | 260 | 'PHOTOGRAPHER' AS booking_type, |
| | 261 | photographer_id AS resource_id, |
| | 262 | start_time, |
| | 263 | end_time |
| | 264 | FROM photographer_booking |
| | 265 | WHERE status = 'CONFIRMED' |
| | 266 | |
| | 267 | UNION ALL |
| | 268 | |
| | 269 | SELECT |
| | 270 | "date", |
| | 271 | 'BAND' AS booking_type, |
| | 272 | band_id AS resource_id, |
| | 273 | start_time, |
| | 274 | end_time |
| | 275 | FROM band_booking |
| | 276 | WHERE status = 'CONFIRMED'; |
| | 277 | }}} |
| | 278 | |
| | 279 | === View 3: Guest count per wedding === |
| | 280 | {{{ |
| | 281 | CREATE VIEW vw_wedding_guest_count AS |
| | 282 | SELECT |
| | 283 | wedding_id, |
| | 284 | COUNT(*) AS total_guests |
| | 285 | FROM guest |
| | 286 | GROUP BY wedding_id; |
| | 287 | }}} |
| | 288 | |
| | 289 | ---- |
| | 290 | |
| | 291 | == Conclusion == |
| | 292 | This phase upgrades our PostgreSQL database with advanced mechanisms used in real systems. |
| | 293 | Indexes improve performance for scheduling, booking checks and RSVP reports. |
| | 294 | Constraints enforce correctness and prevent double bookings and invalid statuses. |
| | 295 | Triggers automate business rules such as venue pricing and capacity limits. |
| | 296 | Views simplify reporting and provide reusable queries for dashboards and analytics. |
| | 297 | |