wiki:AdvancedDB

Version 5 (modified by 221296, 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: Support ticket status consistency + validated ticket attributes

Data requirements description

Support tickets must remain consistent and well-structured throughout their lifecycle:

  • Ticket statuses must follow a controlled set of allowed values.
  • Ticket subjects must not be empty.
  • Ticket handling should support valid status transitions.
  • Ticket data should be easy to inspect together with the related user and administrator information.

Implementation

Stored procedures / functions

CREATE OR REPLACE FUNCTION olpms_is_valid_ticket_transition(p_old text, p_new text)
RETURNS boolean
LANGUAGE sql
AS $$
    SELECT CASE
        WHEN p_old = p_new THEN true
        WHEN p_old = 'OPEN' AND p_new IN ('IN_PROGRESS', 'RESOLVED') THEN true
        WHEN p_old = 'IN_PROGRESS' AND p_new IN ('RESOLVED') THEN true
        WHEN p_old = 'RESOLVED' AND p_new = 'RESOLVED' THEN true
        ELSE false
    END;
$$;

Views

CREATE OR REPLACE VIEW v_support_tickets_enriched AS
SELECT
    st.ticket_id,
    st.subject,
    st.description,
    st.status,
    st.created_at,
    st.user_id,
    u.first_name AS user_first_name,
    u.last_name AS user_last_name,
    u.email AS user_email,
    st.admin_id,
    a.first_name AS admin_first_name,
    a.last_name AS admin_last_name,
    a.email AS admin_email
FROM support_ticket st
JOIN user_entity u
    ON u.id = st.user_id
JOIN user_entity a
    ON a.id = st.admin_id;

Custom domains

CREATE DOMAIN ticket_status_domain AS VARCHAR(30)
CHECK (VALUE IN ('OPEN', 'IN_PROGRESS', 'RESOLVED'));

CREATE DOMAIN non_empty_subject_domain AS VARCHAR(200)
CHECK (LENGTH(TRIM(VALUE)) > 0);
Note: See TracWiki for help on using the wiki.