wiki:AdvancedDB

Version 7 (modified by 221296, 3 days 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.

This ensures that only users with valid subscriptions can access course content, prevents duplicate enrollments, and maintains consistency between subscription and course participation data. It also improves data integrity and reduces the possibility of invalid or inconsistent records in the system.

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.

This ensures that certificates are only issued for successfully completed courses, preventing invalid or premature certification. It also maintains the integrity and reliability of the certification system within the platform.

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.