| Version 4 (modified by , 2 weeks ago) ( diff ) |
|---|
Advanced Database Development
Data constraints requirements: Enrollment-Subscription consistency + single enrollment per course
Data requirements description
Course enrollments must remain consistent with the subscription and participation rules of the platform:
# A user must have an ACTIVE subscription in order to enroll in a course.
# A user cannot enroll in the same course more than once.
# enrollment status changes must follow a valid transition model.
# Each enrollment must reference a valid user and a valid course.
Implementation
Triggers
-- status transition helper
CREATE OR REPLACE FUNCTION olpms_is_valid_enrollment_transition(p_old text, p_new text)
RETURNS boolean
LANGUAGE sql
AS $$
SELECT CASE
WHEN p_old IS NULL AND p_new IN ('IN_PROGRESS', 'COMPLETED', 'CANCELLED') THEN true
WHEN p_old = p_new THEN true
WHEN p_old = 'IN_PROGRESS' AND p_new IN ('COMPLETED', 'CANCELLED') THEN true
WHEN p_old = 'COMPLETED' AND p_new = 'COMPLETED' THEN true
WHEN p_old = 'CANCELLED' AND p_new = 'CANCELLED' THEN true
ELSE false
END;
$$;
-- main enrollment consistency trigger function
CREATE OR REPLACE FUNCTION olpms_trg_enrollment_enforce()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
v_active_subscription_count integer;
BEGIN
-- user must have an active subscription when inserting enrollment
IF TG_OP = 'INSERT' THEN
SELECT COUNT(*)
INTO v_active_subscription_count
FROM user_subscription us
WHERE us.user_id = NEW.user_id
AND us.status = 'ACTIVE'
AND CURRENT_DATE BETWEEN us.start_date AND us.end_date;
IF v_active_subscription_count = 0 THEN
RAISE EXCEPTION 'User % does not have an active subscription', NEW.user_id;
END IF;
END IF;
-- prevent duplicate enrollment for the same user and the same course
IF EXISTS (
SELECT 1
FROM enrollment e
WHERE e.user_id = NEW.user_id
AND e.course_id = NEW.course_id
AND (TG_OP <> 'UPDATE' OR e.enrollment_id <> NEW.enrollment_id)
) THEN
RAISE EXCEPTION 'User % is already enrolled in course %', NEW.user_id, NEW.course_id;
END IF;
-- validate completion_status transition on UPDATE
IF TG_OP = 'UPDATE' THEN
IF NOT olpms_is_valid_enrollment_transition(OLD.completion_status, NEW.completion_status) THEN
RAISE EXCEPTION
'Invalid enrollment status transition: % -> %',
OLD.completion_status,
NEW.completion_status;
END IF;
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_enrollment_enforce ON enrollment;
CREATE TRIGGER trg_enrollment_enforce
BEFORE INSERT OR UPDATE
ON enrollment
FOR EACH ROW
EXECUTE FUNCTION olpms_trg_enrollment_enforce();
Stored procedures / functions
CREATE OR REPLACE FUNCTION olpms_is_valid_enrollment_transition(p_old text, p_new text)
RETURNS boolean
LANGUAGE sql
AS $$
SELECT CASE
WHEN p_old IS NULL AND p_new IN ('IN_PROGRESS', 'COMPLETED', 'CANCELLED') THEN true
WHEN p_old = p_new THEN true
WHEN p_old = 'IN_PROGRESS' AND p_new IN ('COMPLETED', 'CANCELLED') THEN true
WHEN p_old = 'COMPLETED' AND p_new = 'COMPLETED' THEN true
WHEN p_old = 'CANCELLED' AND p_new = 'CANCELLED' THEN true
ELSE false
END;
$$;
CREATE OR REPLACE FUNCTION olpms_trg_enrollment_enforce()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
v_active_subscription_count integer;
BEGIN
IF TG_OP = 'INSERT' THEN
SELECT COUNT(*)
INTO v_active_subscription_count
FROM user_subscription us
WHERE us.user_id = NEW.user_id
AND us.status = 'ACTIVE'
AND CURRENT_DATE BETWEEN us.start_date AND us.end_date;
IF v_active_subscription_count = 0 THEN
RAISE EXCEPTION 'User % does not have an active subscription', NEW.user_id;
END IF;
END IF;
IF EXISTS (
SELECT 1
FROM enrollment e
WHERE e.user_id = NEW.user_id
AND e.course_id = NEW.course_id
AND (TG_OP <> 'UPDATE' OR e.enrollment_id <> NEW.enrollment_id)
) THEN
RAISE EXCEPTION 'User % is already enrolled in course %', NEW.user_id, NEW.course_id;
END IF;
IF TG_OP = 'UPDATE' THEN
IF NOT olpms_is_valid_enrollment_transition(OLD.completion_status, NEW.completion_status) THEN
RAISE EXCEPTION
'Invalid enrollment status transition: % -> %',
OLD.completion_status,
NEW.completion_status;
END IF;
END IF;
RETURN NEW;
END;
$$;
Views
CREATE OR REPLACE VIEW v_enrollments_enriched AS
SELECT
e.enrollment_id,
e.user_id,
ue.first_name,
ue.last_name,
ue.email,
e.course_id,
c.name AS course_name,
e.enroll_date,
e.completion_status,
e.progress_percentage,
us.subscription_id,
us.status AS subscription_status,
us.start_date AS subscription_start_date,
us.end_date AS subscription_end_date
FROM enrollment e
JOIN users u
ON u.id = e.user_id
JOIN user_entity ue
ON ue.id = u.id
JOIN course c
ON c.course_id = e.course_id
LEFT JOIN user_subscription us
ON us.user_id = e.user_id
AND us.status = 'ACTIVE'
AND CURRENT_DATE BETWEEN us.start_date AND us.end_date;
Data constraints requirements: Certificate generation consistency + one certificate per completed enrollment
Data requirements description
Certificates must obey completion and consistency rules:
- A certificate can only be issued for an enrollment whose completion status is COMPLETED.
- The same enrollment cannot have more than one certificate.
- A certificate must always refer to a valid enrollment.
- Issuing a certificate before course completion is blocked.
Implementation
Triggers
CREATE OR REPLACE FUNCTION olpms_trg_certificate_enforce()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
v_completion_status VARCHAR(30);
BEGIN
-- find the status of the referenced enrollment
SELECT e.completion_status
INTO v_completion_status
FROM enrollment e
WHERE e.enrollment_id = NEW.enrollment_id;
IF v_completion_status IS NULL THEN
RAISE EXCEPTION 'Enrollment % does not exist or has no completion status', NEW.enrollment_id;
END IF;
-- certificate can only be issued for completed enrollments
IF v_completion_status <> 'COMPLETED' THEN
RAISE EXCEPTION
'Certificate cannot be issued because enrollment % is not COMPLETED',
NEW.enrollment_id;
END IF;
-- prevent more than one certificate for the same enrollment
IF EXISTS (
SELECT 1
FROM certificate c
WHERE c.enrollment_id = NEW.enrollment_id
AND (TG_OP <> 'UPDATE' OR c.certificate_id <> NEW.certificate_id)
) THEN
RAISE EXCEPTION
'Enrollment % already has a certificate',
NEW.enrollment_id;
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_certificate_enforce ON certificate;
CREATE TRIGGER trg_certificate_enforce
BEFORE INSERT OR UPDATE
ON certificate
FOR EACH ROW
EXECUTE FUNCTION olpms_trg_certificate_enforce();
Views
CREATE OR REPLACE VIEW v_certificates_enriched AS
SELECT
c.certificate_id,
c.enrollment_id,
c.issue_date,
c.certificate_code,
c.status AS certificate_status,
e.user_id,
ue.first_name,
ue.last_name,
ue.email,
e.course_id,
cr.name AS course_name,
e.enroll_date,
e.completion_status,
e.progress_percentage
FROM certificate c
JOIN enrollment e
ON e.enrollment_id = c.enrollment_id
JOIN users u
ON u.id = e.user_id
JOIN user_entity ue
ON ue.id = u.id
JOIN course cr
ON cr.course_id = e.course_id;
Data constraints requirements: Quiz attempt validity + score consistency
Data requirements description
Quiz attempts must obey assessment consistency rules:
- A quiz attempt must always refer to a valid user and a valid quiz.
- The achieved score must be between 0 and the quiz total_points.
- A user should only attempt quizzes that belong to lessons from courses in which the user is enrolled.
- Invalid quiz attempts are blocked
Implementation
Triggers
CREATE OR REPLACE FUNCTION olpms_trg_quiz_attempt_enforce()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
v_total_points INT;
BEGIN
IF NEW.user_id IS NULL THEN
RAISE EXCEPTION 'Quiz attempt must reference a valid user';
END IF;
IF NEW.quiz_id IS NULL THEN
RAISE EXCEPTION 'Quiz attempt must reference a valid quiz';
END IF;
SELECT q.total_points
INTO v_total_points
FROM quiz q
WHERE q.quiz_id = NEW.quiz_id;
IF v_total_points IS NULL THEN
RAISE EXCEPTION 'Quiz % does not exist', NEW.quiz_id;
END IF;
IF NEW.score IS NULL THEN
RAISE EXCEPTION 'Quiz attempt score must not be NULL';
END IF;
IF NEW.score < 0 OR NEW.score > v_total_points THEN
RAISE EXCEPTION
'Invalid score % for quiz %, allowed range is 0 to %',
NEW.score, NEW.quiz_id, v_total_points;
END IF;
IF NOT EXISTS (
SELECT 1
FROM enrollment e
JOIN module m
ON m.course_id = e.course_id
JOIN lesson l
ON l.module_id = m.module_id
JOIN quiz q
ON q.lesson_id = l.lesson_id
WHERE e.user_id = NEW.user_id
AND q.quiz_id = NEW.quiz_id
) THEN
RAISE EXCEPTION
'User % is not enrolled in the course associated with quiz %',
NEW.user_id, NEW.quiz_id;
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_quiz_attempt_enforce ON quiz_attempt;
CREATE TRIGGER trg_quiz_attempt_enforce
BEFORE INSERT OR UPDATE
ON quiz_attempt
FOR EACH ROW
EXECUTE FUNCTION olpms_trg_quiz_attempt_enforce();
Views
CREATE OR REPLACE VIEW v_quiz_attempts_enriched AS
SELECT
qa.attempt_id,
qa.attempt_date,
qa.score,
qa.user_id,
ue.first_name,
ue.last_name,
ue.email,
qa.quiz_id,
q.total_points,
q.passing_score,
l.lesson_id,
l.title AS lesson_title,
m.module_id,
m.title AS module_title,
c.course_id,
c.name AS course_name,
CASE
WHEN qa.score >= q.passing_score THEN 'PASSED'
ELSE 'FAILED'
END AS result_status
FROM quiz_attempt qa
JOIN users u
ON u.id = qa.user_id
JOIN user_entity ue
ON ue.id = u.id
JOIN quiz q
ON q.quiz_id = qa.quiz_id
JOIN lesson l
ON l.lesson_id = q.lesson_id
JOIN module m
ON m.module_id = l.module_id
JOIN course c
ON c.course_id = m.course_id;
Data constraints requirements: Review Consistency
Data requirements description
Reviews are split across multiple tables and require consistency checks:
- A base reviews row can represent either a user_review or a clinic_review, never both.
- A reviewer cannot repeatedly review the same target too frequently (30-day cooldown).
- Prevent self-review.
Implementation
Triggers
CREATE OR REPLACE FUNCTION petify_trg_reviews_no_update()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
RAISE EXCEPTION 'Reviews are immutable. Updates are not allowed (review_id=%).', OLD.review_id;
END;
$$;
DROP TRIGGER IF EXISTS trg_reviews_no_update ON reviews;
CREATE TRIGGER trg_reviews_no_update
BEFORE UPDATE
ON reviews
FOR EACH ROW
EXECUTE FUNCTION petify_trg_reviews_no_update();
DROP TRIGGER IF EXISTS trg_clinic_reviews_no_update ON clinic_reviews;
CREATE TRIGGER trg_clinic_reviews_no_update
BEFORE UPDATE
ON clinic_reviews
FOR EACH ROW
EXECUTE FUNCTION petify_trg_no_update_generic();
CREATE OR REPLACE FUNCTION petify_trg_user_review_exclusive()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF EXISTS (SELECT 1 FROM clinic_reviews cr WHERE cr.review_id = NEW.review_id) THEN
RAISE EXCEPTION 'review_id % already used as clinic review (cannot also be user review)', NEW.review_id;
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_user_review_exclusive ON user_reviews;
CREATE TRIGGER trg_user_review_exclusive
BEFORE INSERT
ON user_reviews
FOR EACH ROW
EXECUTE FUNCTION petify_trg_user_review_exclusive();
CREATE OR REPLACE FUNCTION petify_trg_clinic_review_exclusive()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF EXISTS (SELECT 1 FROM user_reviews ur WHERE ur.review_id = NEW.review_id) THEN
RAISE EXCEPTION 'review_id % already used as user review (cannot also be clinic review)', NEW.review_id;
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_clinic_review_exclusive ON clinic_reviews;
CREATE TRIGGER trg_clinic_review_exclusive
BEFORE INSERT
ON clinic_reviews
FOR EACH ROW
EXECUTE FUNCTION petify_trg_clinic_review_exclusive();
CREATE OR REPLACE FUNCTION petify_trg_user_reviews_cooldown()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
v_reviewer bigint;
v_created timestamp;
BEGIN
SELECT reviewer_id, created_at INTO v_reviewer, v_created
FROM reviews
WHERE review_id = NEW.review_id;
IF v_reviewer IS NULL THEN
RAISE EXCEPTION 'Base review % not found', NEW.review_id;
END IF;
IF v_reviewer = NEW.target_user_id THEN
RAISE EXCEPTION 'User cannot review themselves (user_id=%)', v_reviewer;
END IF;
IF EXISTS (
SELECT 1
FROM user_reviews ur
JOIN reviews r ON r.review_id = ur.review_id
WHERE r.reviewer_id = v_reviewer
AND ur.target_user_id = NEW.target_user_id
AND r.is_deleted = false
AND r.created_at >= v_created - interval '30 days'
) THEN
RAISE EXCEPTION 'Cooldown: reviewer % already reviewed user % within last 30 days',
v_reviewer, NEW.target_user_id;
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_user_reviews_cooldown ON user_reviews;
CREATE TRIGGER trg_user_reviews_cooldown
BEFORE INSERT
ON user_reviews
FOR EACH ROW
EXECUTE FUNCTION petify_trg_user_reviews_cooldown();
CREATE OR REPLACE FUNCTION petify_trg_clinic_reviews_cooldown()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
v_reviewer bigint;
v_created timestamp;
BEGIN
SELECT reviewer_id, created_at INTO v_reviewer, v_created
FROM reviews
WHERE review_id = NEW.review_id;
IF v_reviewer IS NULL THEN
RAISE EXCEPTION 'Base review % not found', NEW.review_id;
END IF;
IF EXISTS (
SELECT 1
FROM clinic_reviews cr
JOIN reviews r ON r.review_id = cr.review_id
WHERE r.reviewer_id = v_reviewer
AND cr.target_clinic_id = NEW.target_clinic_id
AND r.is_deleted = false
AND r.created_at >= v_created - interval '30 days'
) THEN
RAISE EXCEPTION 'Cooldown: reviewer % already reviewed clinic % within last 30 days',
v_reviewer, NEW.target_clinic_id;
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_clinic_reviews_cooldown ON clinic_reviews;
CREATE TRIGGER trg_clinic_reviews_cooldown
BEFORE INSERT
ON clinic_reviews
FOR EACH ROW
EXECUTE FUNCTION petify_trg_clinic_reviews_cooldown();
Views
CREATE OR REPLACE VIEW v_user_ratings AS SELECT ur.target_user_id, COUNT(*) FILTER (WHERE r.is_deleted = false) AS review_count, ROUND(AVG(r.rating)::numeric, 2) FILTER (WHERE r.is_deleted = false) AS avg_rating FROM user_reviews ur JOIN reviews r ON r.review_id = ur.review_id GROUP BY ur.target_user_id; CREATE OR REPLACE VIEW v_clinic_ratings AS SELECT cr.target_clinic_id, COUNT(*) FILTER (WHERE r.is_deleted = false) AS review_count, ROUND(AVG(r.rating)::numeric, 2) FILTER (WHERE r.is_deleted = false) AS avg_rating FROM clinic_reviews cr JOIN reviews r ON r.review_id = cr.review_id GROUP BY cr.target_clinic_id;
Custom domains
CREATE DOMAIN rating_1_5 AS int CHECK (VALUE BETWEEN 1 AND 5);
Data constraints requirements: Background Jobs
Data requirements description
These are time-based business rules that must be enforced asynchronously:
- If an appointment is still CONFIRMED long after its scheduled time, mark it as NO_SHOW.
- If an Archive listing is older than 30days its status is draft.
Implementation
Stored procedures
CREATE EXTENSION IF NOT EXISTS pg_cron;
CREATE OR REPLACE PROCEDURE job_mark_no_show()
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE appointments
SET status = 'NO_SHOW'
WHERE status = 'CONFIRMED'
AND date_time < now() - interval '45 minutes';
END;
$$;
CREATE OR REPLACE PROCEDURE job_archive_stale_drafts()
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE listings
SET status = 'ARCHIVED'
WHERE status = 'DRAFT'
AND created_at < now() - interval '30 days';
END;
$$;
Views
CREATE OR REPLACE VIEW v_overdue_confirmed_appointments AS SELECT * FROM appointments WHERE status='CONFIRMED' AND date_time < now() - interval '45 minutes'; CREATE OR REPLACE VIEW v_stale_draft_listings AS SELECT * FROM listings WHERE status='DRAFT' AND created_at < now() - interval '30 days';
Scheduling
SELECT cron.schedule('petify_mark_no_show', '*/10 * * * *', $$CALL job_mark_no_show();$$);
SELECT cron.schedule('petify_archive_stale_drafts_daily', '10 2 * * *', $$CALL job_archive_stale_drafts();$$);
