Changes between Initial Version and Version 1 of AdvancedDatabaseDevelopment


Ignore:
Timestamp:
07/07/26 23:19:01 (2 days ago)
Author:
231035
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • AdvancedDatabaseDevelopment

    v1 v1  
     1= Advanced Database Development
     2== Data constrains requirements: Listing-Animal ownership consistency + single active listing
     3=== Data requirements description
     4Petify listings must stay consistent with the animal they refer to:
     5* listings.owner_id must always match animals.owner_id for the same animal_id.
     6* An animal can have at most one ACTIVE listing at a time.
     7* Listing status changes must follow a valid transition model.
     8=== Implementation
     9{{{Triggers}}}
     10{{{
     11-- status transition
     12CREATE OR REPLACE FUNCTION petify_is_valid_listing_transition(p_old text, p_new text)
     13RETURNS boolean
     14LANGUAGE sql
     15AS $$
     16  SELECT CASE
     17    WHEN p_old = p_new THEN true
     18    WHEN p_old = 'DRAFT'    AND p_new IN ('ACTIVE','ARCHIVED') THEN true
     19    WHEN p_old = 'ACTIVE'   AND p_new IN ('SOLD','ARCHIVED')   THEN true
     20    WHEN p_old = 'SOLD'     AND p_new IN ('ARCHIVED')          THEN true
     21    WHEN p_old = 'ARCHIVED' THEN false
     22    ELSE false
     23  END;
     24$$;
     25
     26-- same owner
     27CREATE OR REPLACE FUNCTION petify_trg_listings_enforce()
     28RETURNS trigger
     29LANGUAGE plpgsql
     30AS $$
     31DECLARE
     32  v_animal_owner bigint;
     33BEGIN
     34  SELECT owner_id INTO v_animal_owner
     35  FROM animals
     36  WHERE animal_id = NEW.animal_id;
     37
     38  IF v_animal_owner IS NULL THEN
     39    RAISE EXCEPTION 'Animal % not found', NEW.animal_id;
     40  END IF;
     41
     42  IF NEW.owner_id <> v_animal_owner THEN
     43    RAISE EXCEPTION 'Listing owner_id (%) must match animal.owner_id (%) for animal %',
     44      NEW.owner_id, v_animal_owner, NEW.animal_id;
     45  END IF;
     46
     47  IF TG_OP = 'UPDATE' THEN
     48    IF NOT petify_is_valid_listing_transition(OLD.status, NEW.status) THEN
     49      RAISE EXCEPTION 'Invalid listing status transition: % -> %', OLD.status, NEW.status;
     50    END IF;
     51  END IF;
     52
     53  IF NEW.status = 'ACTIVE' THEN
     54    IF EXISTS (
     55      SELECT 1
     56      FROM listings l
     57      WHERE l.animal_id = NEW.animal_id
     58        AND l.status = 'ACTIVE'
     59        AND (TG_OP <> 'UPDATE' OR l.listing_id <> NEW.listing_id)
     60    ) THEN
     61      RAISE EXCEPTION 'Animal % already has an ACTIVE listing', NEW.animal_id;
     62    END IF;
     63  END IF;
     64
     65  RETURN NEW;
     66END;
     67$$;
     68
     69DROP TRIGGER IF EXISTS trg_listings_enforce ON listings;
     70CREATE TRIGGER trg_listings_enforce
     71BEFORE INSERT OR UPDATE
     72ON listings
     73FOR EACH ROW
     74EXECUTE FUNCTION petify_trg_listings_enforce();
     75
     76}}}
     77{{{Stored procedures}}}
     78{{{
     79CREATE OR REPLACE VIEW v_listings_enriched AS
     80SELECT
     81  l.listing_id,
     82  l.status,
     83  l.price,
     84  l.created_at,
     85  l.owner_id AS listing_owner_id,
     86  a.animal_id,
     87  a.name AS animal_name,
     88  a.species,
     89  a.breed,
     90  a.located_name,
     91  a.owner_id AS animal_owner_id,
     92  (l.owner_id = a.owner_id) AS owner_match
     93FROM listings l
     94JOIN animals a ON a.animal_id = l.animal_id;
     95}}}
     96== Data constraints requirements: Appointment owner consistency, no overlap scheduling
     97=== Data requirements description
     98Appointments must obey real scheduling constraints:
     99* The responsible_owner_id for an appointment must match the owner of the animal.
     100* The same owner cannot have overlapping appointments.
     101* The same animal cannot have overlapping appointments.
     102* Confirming appointments in the past is blocked.
     103=== Implementation
     104{{{Triggers}}}
     105{{{
     106CREATE OR REPLACE FUNCTION petify_trg_appointments_enforce()
     107RETURNS trigger
     108LANGUAGE plpgsql
     109AS $$
     110DECLARE
     111  v_animal_owner bigint;
     112BEGIN
     113  SELECT owner_id INTO v_animal_owner
     114  FROM animals
     115  WHERE animal_id = NEW.animal_id;
     116
     117  IF v_animal_owner IS NULL THEN
     118    RAISE EXCEPTION 'Animal % not found for appointment', NEW.animal_id;
     119  END IF;
     120
     121  IF NEW.responsible_owner_id <> v_animal_owner THEN
     122    RAISE EXCEPTION
     123      'Appointment responsible_owner_id (%) must match animals.owner_id (%) for animal %',
     124      NEW.responsible_owner_id, v_animal_owner, NEW.animal_id;
     125  END IF;
     126
     127  IF NEW.status = 'CONFIRMED' AND NEW.date_time < now() THEN
     128    RAISE EXCEPTION 'Cannot CONFIRM an appointment in the past (date_time=%)', NEW.date_time;
     129  END IF;
     130
     131  IF NEW.status = 'DONE' AND NEW.date_time > now() THEN
     132    RAISE EXCEPTION 'Cannot mark DONE for an appointment that is in the future (date_time=%)', NEW.date_time;
     133  END IF;
     134
     135  RETURN NEW;
     136END;
     137$$;
     138
     139DO $$
     140BEGIN
     141  IF NOT EXISTS (
     142    SELECT 1
     143    FROM information_schema.columns
     144    WHERE table_name = 'appointments'
     145      AND column_name = 'slot'
     146  ) THEN
     147    ALTER TABLE appointments
     148      ADD COLUMN slot tstzrange
     149      GENERATED ALWAYS AS (
     150        tstzrange(date_time, date_time + interval '30 minutes', '[)')
     151      ) STORED;
     152  END IF;
     153END $$;
     154
     155CREATE OR REPLACE FUNCTION petify_trg_appointments_no_overlap()
     156RETURNS trigger
     157LANGUAGE plpgsql
     158AS $$
     159BEGIN
     160 
     161  IF NEW.status NOT IN ('CONFIRMED','DONE') THEN
     162    RETURN NEW;
     163  END IF;
     164
     165
     166  IF EXISTS (
     167    SELECT 1
     168    FROM appointments a
     169    WHERE a.responsible_owner_id = NEW.responsible_owner_id
     170      AND a.status IN ('CONFIRMED','DONE')
     171      AND a.slot && NEW.slot
     172      AND (TG_OP <> 'UPDATE' OR a.appointment_id <> NEW.appointment_id)
     173  ) THEN
     174    RAISE EXCEPTION
     175      'Overlapping appointment for owner % at %',
     176      NEW.responsible_owner_id, NEW.date_time;
     177  END IF;
     178
     179 
     180  IF EXISTS (
     181    SELECT 1
     182    FROM appointments a
     183    WHERE a.animal_id = NEW.animal_id
     184      AND a.status IN ('CONFIRMED','DONE')
     185      AND a.slot && NEW.slot
     186      AND (TG_OP <> 'UPDATE' OR a.appointment_id <> NEW.appointment_id)
     187  ) THEN
     188    RAISE EXCEPTION
     189      'Overlapping appointment for animal % at %',
     190      NEW.animal_id, NEW.date_time;
     191  END IF;
     192
     193  RETURN NEW;
     194END;
     195$$;
     196
     197
     198DROP TRIGGER IF EXISTS trg_appointments_enforce ON appointments;
     199CREATE TRIGGER trg_appointments_enforce
     200BEFORE INSERT OR UPDATE
     201ON appointments
     202FOR EACH ROW
     203EXECUTE FUNCTION petify_trg_appointments_enforce();
     204
     205DROP TRIGGER IF EXISTS trg_appointments_no_overlap ON appointments;
     206CREATE TRIGGER trg_appointments_no_overlap
     207BEFORE INSERT OR UPDATE
     208ON appointments
     209FOR EACH ROW
     210EXECUTE FUNCTION petify_trg_appointments_no_overlap();
     211
     212}}}
     213
     214{{{Views}}}
     215{{{
     216CREATE OR REPLACE VIEW v_clinic_appointments_monthly AS
     217SELECT
     218  clinic_id,
     219  date_trunc('month', date_time) AS month,
     220  COUNT(*) FILTER (WHERE status='CONFIRMED') AS confirmed_cnt,
     221  COUNT(*) FILTER (WHERE status='DONE')      AS done_cnt,
     222  COUNT(*) FILTER (WHERE status='NO_SHOW')   AS no_show_cnt,
     223  COUNT(*) FILTER (WHERE status='CANCELLED') AS cancelled_cnt,
     224  COUNT(*) AS total_cnt
     225FROM appointments
     226GROUP BY clinic_id, date_trunc('month', date_time)
     227ORDER BY month DESC;
     228
     229}}}
     230== Data constraints requirements: Health Record integrity
     231=== Data requirements description
     232Health records must be consistent:
     233* A health record must be tied to an appointment that is DONE.
     234* The animal in health_records must match the animal of the referenced appointment.
     235* The record date must match the appointment date.
     236=== Implementation
     237{{{Triggers}}}
     238{{{
     239CREATE OR REPLACE FUNCTION petify_trg_health_records_enforce()
     240RETURNS trigger
     241LANGUAGE plpgsql
     242AS $$
     243DECLARE
     244  v_appt_animal bigint;
     245  v_appt_status text;
     246  v_appt_date date;
     247BEGIN
     248  SELECT a.animal_id, a.status, a.date_time::date
     249  INTO v_appt_animal, v_appt_status, v_appt_date
     250  FROM appointments a
     251  WHERE a.appointment_id = NEW.appointment_id;
     252
     253  IF v_appt_animal IS NULL THEN
     254    RAISE EXCEPTION 'Appointment % not found for health record', NEW.appointment_id;
     255  END IF;
     256
     257  IF NEW.animal_id <> v_appt_animal THEN
     258    RAISE EXCEPTION 'Health record animal_id (%) must match appointment animal_id (%) for appointment %',
     259      NEW.animal_id, v_appt_animal, NEW.appointment_id;
     260  END IF;
     261
     262  IF v_appt_status <> 'DONE' THEN
     263    RAISE EXCEPTION 'Cannot insert health record unless appointment % is DONE (current status=%)',
     264      NEW.appointment_id, v_appt_status;
     265  END IF;
     266
     267  IF NEW.date <> v_appt_date THEN
     268    RAISE EXCEPTION 'Health record date (%) must equal appointment date (%) for appointment %',
     269      NEW.date, v_appt_date, NEW.appointment_id;
     270  END IF;
     271
     272  RETURN NEW;
     273END;
     274$$;
     275
     276DROP TRIGGER IF EXISTS trg_health_records_enforce ON health_records;
     277CREATE TRIGGER trg_health_records_enforce
     278BEFORE INSERT OR UPDATE
     279ON health_records
     280FOR EACH ROW
     281EXECUTE FUNCTION petify_trg_health_records_enforce();
     282}}}
     283{{{Views}}}
     284{{{
     285CREATE OR REPLACE VIEW v_health_records_with_context AS
     286SELECT
     287  hr.healthrecord_id,
     288  hr.animal_id,
     289  a.name AS animal_name,
     290  hr.appointment_id,
     291  ap.clinic_id,
     292  ap.date_time,
     293  ap.status AS appointment_status,
     294  hr.type,
     295  hr.description,
     296  hr.date
     297FROM health_records hr
     298JOIN animals a      ON a.animal_id = hr.animal_id
     299JOIN appointments ap ON ap.appointment_id = hr.appointment_id;
     300}}}
     301== Data constraints requirements: Review Consistency
     302=== Data requirements description
     303Reviews are split across multiple tables and require consistency checks:
     304* A base reviews row can represent either a user_review or a clinic_review, never both.
     305* A reviewer cannot repeatedly review the same target too frequently (30-day cooldown).
     306* Prevent self-review.
     307=== Implementation
     308{{{Triggers}}}
     309{{{
     310CREATE OR REPLACE FUNCTION petify_trg_reviews_no_update()
     311RETURNS trigger
     312LANGUAGE plpgsql
     313AS $$
     314BEGIN
     315  RAISE EXCEPTION 'Reviews are immutable. Updates are not allowed (review_id=%).', OLD.review_id;
     316END;
     317$$;
     318
     319DROP TRIGGER IF EXISTS trg_reviews_no_update ON reviews;
     320CREATE TRIGGER trg_reviews_no_update
     321BEFORE UPDATE
     322ON reviews
     323FOR EACH ROW
     324EXECUTE FUNCTION petify_trg_reviews_no_update();
     325
     326DROP TRIGGER IF EXISTS trg_clinic_reviews_no_update ON clinic_reviews;
     327CREATE TRIGGER trg_clinic_reviews_no_update
     328BEFORE UPDATE
     329ON clinic_reviews
     330FOR EACH ROW
     331EXECUTE FUNCTION petify_trg_no_update_generic();
     332
     333CREATE OR REPLACE FUNCTION petify_trg_user_review_exclusive()
     334RETURNS trigger
     335LANGUAGE plpgsql
     336AS $$
     337BEGIN
     338  IF EXISTS (SELECT 1 FROM clinic_reviews cr WHERE cr.review_id = NEW.review_id) THEN
     339    RAISE EXCEPTION 'review_id % already used as clinic review (cannot also be user review)', NEW.review_id;
     340  END IF;
     341  RETURN NEW;
     342END;
     343$$;
     344
     345DROP TRIGGER IF EXISTS trg_user_review_exclusive ON user_reviews;
     346CREATE TRIGGER trg_user_review_exclusive
     347BEFORE INSERT
     348ON user_reviews
     349FOR EACH ROW
     350EXECUTE FUNCTION petify_trg_user_review_exclusive();
     351
     352
     353CREATE OR REPLACE FUNCTION petify_trg_clinic_review_exclusive()
     354RETURNS trigger
     355LANGUAGE plpgsql
     356AS $$
     357BEGIN
     358  IF EXISTS (SELECT 1 FROM user_reviews ur WHERE ur.review_id = NEW.review_id) THEN
     359    RAISE EXCEPTION 'review_id % already used as user review (cannot also be clinic review)', NEW.review_id;
     360  END IF;
     361  RETURN NEW;
     362END;
     363$$;
     364
     365DROP TRIGGER IF EXISTS trg_clinic_review_exclusive ON clinic_reviews;
     366CREATE TRIGGER trg_clinic_review_exclusive
     367BEFORE INSERT
     368ON clinic_reviews
     369FOR EACH ROW
     370EXECUTE FUNCTION petify_trg_clinic_review_exclusive();
     371
     372CREATE OR REPLACE FUNCTION petify_trg_user_reviews_cooldown()
     373RETURNS trigger
     374LANGUAGE plpgsql
     375AS $$
     376DECLARE
     377  v_reviewer bigint;
     378  v_created  timestamp;
     379BEGIN
     380  SELECT reviewer_id, created_at INTO v_reviewer, v_created
     381  FROM reviews
     382  WHERE review_id = NEW.review_id;
     383
     384  IF v_reviewer IS NULL THEN
     385    RAISE EXCEPTION 'Base review % not found', NEW.review_id;
     386  END IF;
     387
     388  IF v_reviewer = NEW.target_user_id THEN
     389    RAISE EXCEPTION 'User cannot review themselves (user_id=%)', v_reviewer;
     390  END IF;
     391
     392  IF EXISTS (
     393    SELECT 1
     394    FROM user_reviews ur
     395    JOIN reviews r ON r.review_id = ur.review_id
     396    WHERE r.reviewer_id = v_reviewer
     397      AND ur.target_user_id = NEW.target_user_id
     398      AND r.is_deleted = false
     399      AND r.created_at >= v_created - interval '30 days'
     400  ) THEN
     401    RAISE EXCEPTION 'Cooldown: reviewer % already reviewed user % within last 30 days',
     402      v_reviewer, NEW.target_user_id;
     403  END IF;
     404
     405  RETURN NEW;
     406END;
     407$$;
     408
     409DROP TRIGGER IF EXISTS trg_user_reviews_cooldown ON user_reviews;
     410CREATE TRIGGER trg_user_reviews_cooldown
     411BEFORE INSERT
     412ON user_reviews
     413FOR EACH ROW
     414EXECUTE FUNCTION petify_trg_user_reviews_cooldown();
     415
     416CREATE OR REPLACE FUNCTION petify_trg_clinic_reviews_cooldown()
     417RETURNS trigger
     418LANGUAGE plpgsql
     419AS $$
     420DECLARE
     421  v_reviewer bigint;
     422  v_created  timestamp;
     423BEGIN
     424  SELECT reviewer_id, created_at INTO v_reviewer, v_created
     425  FROM reviews
     426  WHERE review_id = NEW.review_id;
     427
     428  IF v_reviewer IS NULL THEN
     429    RAISE EXCEPTION 'Base review % not found', NEW.review_id;
     430  END IF;
     431
     432  IF EXISTS (
     433    SELECT 1
     434    FROM clinic_reviews cr
     435    JOIN reviews r ON r.review_id = cr.review_id
     436    WHERE r.reviewer_id = v_reviewer
     437      AND cr.target_clinic_id = NEW.target_clinic_id
     438      AND r.is_deleted = false
     439      AND r.created_at >= v_created - interval '30 days'
     440  ) THEN
     441    RAISE EXCEPTION 'Cooldown: reviewer % already reviewed clinic % within last 30 days',
     442      v_reviewer, NEW.target_clinic_id;
     443  END IF;
     444
     445  RETURN NEW;
     446END;
     447$$;
     448
     449DROP TRIGGER IF EXISTS trg_clinic_reviews_cooldown ON clinic_reviews;
     450CREATE TRIGGER trg_clinic_reviews_cooldown
     451BEFORE INSERT
     452ON clinic_reviews
     453FOR EACH ROW
     454EXECUTE FUNCTION petify_trg_clinic_reviews_cooldown();
     455
     456}}}
     457{{{Views}}}
     458{{{
     459CREATE OR REPLACE VIEW v_user_ratings AS
     460SELECT
     461  ur.target_user_id,
     462  COUNT(*) FILTER (WHERE r.is_deleted = false) AS review_count,
     463  ROUND(AVG(r.rating)::numeric, 2) FILTER (WHERE r.is_deleted = false) AS avg_rating
     464FROM user_reviews ur
     465JOIN reviews r ON r.review_id = ur.review_id
     466GROUP BY ur.target_user_id;
     467
     468CREATE OR REPLACE VIEW v_clinic_ratings AS
     469SELECT
     470  cr.target_clinic_id,
     471  COUNT(*) FILTER (WHERE r.is_deleted = false) AS review_count,
     472  ROUND(AVG(r.rating)::numeric, 2) FILTER (WHERE r.is_deleted = false) AS avg_rating
     473FROM clinic_reviews cr
     474JOIN reviews r ON r.review_id = cr.review_id
     475GROUP BY cr.target_clinic_id;
     476}}}
     477{{{Custom domains}}}
     478{{{
     479CREATE DOMAIN rating_1_5 AS int CHECK (VALUE BETWEEN 1 AND 5);
     480}}}
     481== Data constraints requirements: Background Jobs
     482=== Data requirements description
     483These are time-based business rules that must be enforced asynchronously:
     484* If an appointment is still CONFIRMED long after its scheduled time, mark it as NO_SHOW.
     485* If an Archive listing is older than 30days its status is draft.
     486=== Implementation
     487{{{Stored procedures}}}
     488{{{
     489CREATE EXTENSION IF NOT EXISTS pg_cron;
     490
     491CREATE OR REPLACE PROCEDURE job_mark_no_show()
     492LANGUAGE plpgsql
     493AS $$
     494BEGIN
     495  UPDATE appointments
     496  SET status = 'NO_SHOW'
     497  WHERE status = 'CONFIRMED'
     498    AND date_time < now() - interval '45 minutes';
     499END;
     500$$;
     501
     502CREATE OR REPLACE PROCEDURE job_archive_stale_drafts()
     503LANGUAGE plpgsql
     504AS $$
     505BEGIN
     506  UPDATE listings
     507  SET status = 'ARCHIVED'
     508  WHERE status = 'DRAFT'
     509    AND created_at < now() - interval '30 days';
     510END;
     511$$;
     512}}}
     513{{{Views}}}
     514{{{
     515CREATE OR REPLACE VIEW v_overdue_confirmed_appointments AS
     516SELECT *
     517FROM appointments
     518WHERE status='CONFIRMED'
     519  AND date_time < now() - interval '45 minutes';
     520
     521CREATE OR REPLACE VIEW v_stale_draft_listings AS
     522SELECT *
     523FROM listings
     524WHERE status='DRAFT'
     525  AND created_at < now() - interval '30 days';
     526}}}
     527{{{Scheduling}}}
     528{{{
     529SELECT cron.schedule('petify_mark_no_show', '*/10 * * * *', $$CALL job_mark_no_show();$$);
     530SELECT cron.schedule('petify_archive_stale_drafts_daily', '10 2 * * *', $$CALL job_archive_stale_drafts();$$);
     531}}}