Changes between Initial Version and Version 1 of AdvancedDB


Ignore:
Timestamp:
03/28/26 12:41:42 (2 weeks ago)
Author:
221296
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • AdvancedDB

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