Week 15 — Advanced Project Implementation & Capstone Review¶
Course Objectives: CO9 | Focus: Capstone Project, Synthesis, Career Paths | Difficulty: ⭐⭐⭐⭐⭐
Learning Objectives¶
By the end of this week, you should be able to:
- [ ] Design and implement a fully normalised 8-table schema for a real-world registration system
- [ ] Write advanced stored procedures with cursor iteration, conditional logic, and error signalling
- [ ] Implement BEFORE and AFTER triggers that enforce business rules and maintain audit trails
- [ ] Create user-defined functions for computed values used across views and procedures
- [ ] Apply a complete role-based security model with granular privilege grants
- [ ] Define views that encapsulate complex joins and serve specific application layers
- [ ] Justify every index with EXPLAIN output analysis and explain the trade-off
- [ ] Write a Python ETL script that validates and imports enrollment data from CSV
- [ ] Configure and verify Event Scheduler events for nightly and semester-end automation
- [ ] Articulate how all 15 weeks of the course connect into professional database engineering practice
1. Capstone Project Overview — University Course Registration System¶
This capstone delivers a production-quality database for Frostburg State University's Course Registration System. It integrates every major topic covered in ITEC 445: schema design, stored procedures, triggers, functions, security, views, indexing, scripting, and automation.
Project Scope
System: FCRS — Frostburg Course Registration System
Database: fcrs (MySQL 8.0)
Tables: 9 core tables + 1 audit table
Users/Roles: 4 roles with distinct privilege sets
Deliverables: Schema DDL, procedures, triggers, functions, views, indexes, Python importer, backup script, event scheduler, health check procedure, project documentation
2. Full Normalised Schema¶
2.1 Entity-Relationship Overview¶
departments ──< courses ──< sections ──< enrollments >── students
│
grades ─── (1:1 per enrollment)
instructors >── sections (instructor assigned to section)
courses >── prerequisites (self-referential many-to-many)
audit_log ──── (records all sensitive changes)
2.2 CREATE TABLE Statements¶
-- ============================================================
-- FCRS: Frostburg Course Registration System
-- MySQL 8.0+
-- ============================================================
CREATE DATABASE IF NOT EXISTS fcrs
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;
USE fcrs;
SET FOREIGN_KEY_CHECKS = 0;
-- ============================================================
-- TABLE: departments
-- ============================================================
CREATE TABLE IF NOT EXISTS departments (
dept_id TINYINT UNSIGNED NOT NULL AUTO_INCREMENT,
dept_code CHAR(4) NOT NULL COMMENT 'e.g. ITEC, MATH, ENGL',
dept_name VARCHAR(80) NOT NULL,
building VARCHAR(40) NULL,
chair_id INT NULL COMMENT 'FK -> instructors; set after instructors loaded',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (dept_id),
UNIQUE KEY uq_dept_code (dept_code)
) ENGINE=InnoDB COMMENT='Academic departments';
-- ============================================================
-- TABLE: instructors
-- ============================================================
CREATE TABLE IF NOT EXISTS instructors (
instructor_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
email VARCHAR(120) NOT NULL,
dept_id TINYINT UNSIGNED NOT NULL,
title ENUM('Instructor','Asst Professor','Assoc Professor',
'Professor','Adjunct','Lecturer')
NOT NULL DEFAULT 'Instructor',
hired_date DATE NOT NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
PRIMARY KEY (instructor_id),
UNIQUE KEY uq_instructor_email (email),
KEY idx_instructor_dept (dept_id),
CONSTRAINT fk_instr_dept FOREIGN KEY (dept_id)
REFERENCES departments(dept_id) ON UPDATE CASCADE
) ENGINE=InnoDB COMMENT='Faculty and instructors';
-- Now set chair FK (deferred to avoid circular ref)
ALTER TABLE departments
ADD CONSTRAINT fk_dept_chair FOREIGN KEY (chair_id)
REFERENCES instructors(instructor_id) ON DELETE SET NULL;
-- ============================================================
-- TABLE: students
-- ============================================================
CREATE TABLE IF NOT EXISTS students (
student_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
fsuid CHAR(9) NOT NULL COMMENT 'FSU student ID e.g. B00123456',
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
preferred_name VARCHAR(50) NULL,
email VARCHAR(120) NOT NULL,
dept_id TINYINT UNSIGNED NULL COMMENT 'Declared major department',
gpa DECIMAL(4,3) NOT NULL DEFAULT 0.000,
credit_hours SMALLINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Total completed credit hours',
enrollment_status ENUM('active','suspended','withdrawn','graduated','applicant')
NOT NULL DEFAULT 'active',
admit_date DATE NOT NULL,
grad_date DATE NULL,
PRIMARY KEY (student_id),
UNIQUE KEY uq_student_fsuid (fsuid),
UNIQUE KEY uq_student_email (email),
KEY idx_student_dept (dept_id),
KEY idx_student_status (enrollment_status),
CONSTRAINT fk_student_dept FOREIGN KEY (dept_id)
REFERENCES departments(dept_id) ON UPDATE CASCADE ON DELETE SET NULL
) ENGINE=InnoDB COMMENT='Registered students';
-- ============================================================
-- TABLE: courses
-- ============================================================
CREATE TABLE IF NOT EXISTS courses (
course_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
dept_id TINYINT UNSIGNED NOT NULL,
course_code VARCHAR(10) NOT NULL COMMENT 'e.g. ITEC445',
title VARCHAR(120) NOT NULL,
description TEXT NULL,
credit_hours TINYINT UNSIGNED NOT NULL DEFAULT 3,
level ENUM('100','200','300','400','500','600')
NOT NULL DEFAULT '400',
is_active TINYINT(1) NOT NULL DEFAULT 1,
PRIMARY KEY (course_id),
UNIQUE KEY uq_course_code (course_code),
KEY idx_course_dept (dept_id),
CONSTRAINT fk_course_dept FOREIGN KEY (dept_id)
REFERENCES departments(dept_id) ON UPDATE CASCADE
) ENGINE=InnoDB COMMENT='Course catalogue';
-- ============================================================
-- TABLE: prerequisites (self-referential many-to-many)
-- ============================================================
CREATE TABLE IF NOT EXISTS prerequisites (
course_id INT UNSIGNED NOT NULL COMMENT 'Course that has prereqs',
prereq_id INT UNSIGNED NOT NULL COMMENT 'Required prerequisite course',
min_grade CHAR(2) NOT NULL DEFAULT 'D' COMMENT 'Minimum passing grade',
PRIMARY KEY (course_id, prereq_id),
CONSTRAINT fk_prereq_course FOREIGN KEY (course_id)
REFERENCES courses(course_id) ON DELETE CASCADE,
CONSTRAINT fk_prereq_prereq FOREIGN KEY (prereq_id)
REFERENCES courses(course_id) ON DELETE CASCADE
) ENGINE=InnoDB COMMENT='Course prerequisite relationships';
-- ============================================================
-- TABLE: sections
-- ============================================================
CREATE TABLE IF NOT EXISTS sections (
section_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
course_id INT UNSIGNED NOT NULL,
instructor_id INT UNSIGNED NULL,
section_num TINYINT UNSIGNED NOT NULL DEFAULT 1,
semester VARCHAR(10) NOT NULL COMMENT 'e.g. Fall2025',
schedule_days SET('M','T','W','R','F','S')
NOT NULL,
start_time TIME NOT NULL,
end_time TIME NOT NULL,
room VARCHAR(20) NULL,
capacity TINYINT UNSIGNED NOT NULL DEFAULT 30,
enrolled_count TINYINT UNSIGNED NOT NULL DEFAULT 0,
delivery_mode ENUM('in-person','online','hybrid')
NOT NULL DEFAULT 'in-person',
status ENUM('open','closed','cancelled','waitlist')
NOT NULL DEFAULT 'open',
PRIMARY KEY (section_id),
UNIQUE KEY uq_section (course_id, section_num, semester),
KEY idx_section_semester (semester),
KEY idx_section_instructor (instructor_id),
CONSTRAINT fk_section_course FOREIGN KEY (course_id)
REFERENCES courses(course_id) ON UPDATE CASCADE,
CONSTRAINT fk_section_instructor FOREIGN KEY (instructor_id)
REFERENCES instructors(instructor_id) ON DELETE SET NULL
) ENGINE=InnoDB COMMENT='Scheduled course sections';
-- ============================================================
-- TABLE: enrollments
-- ============================================================
CREATE TABLE IF NOT EXISTS enrollments (
enrollment_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
student_id INT UNSIGNED NOT NULL,
section_id INT UNSIGNED NOT NULL,
enrolled_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
status ENUM('enrolled','dropped','withdrawn','waitlisted','completed')
NOT NULL DEFAULT 'enrolled',
drop_date DATE NULL,
PRIMARY KEY (enrollment_id),
UNIQUE KEY uq_enrollment (student_id, section_id),
KEY idx_enrollment_section (section_id),
KEY idx_enrollment_student (student_id),
KEY idx_enrollment_status (status),
KEY idx_enrollment_semester (section_id, status),
CONSTRAINT fk_enroll_student FOREIGN KEY (student_id)
REFERENCES students(student_id) ON UPDATE CASCADE,
CONSTRAINT fk_enroll_section FOREIGN KEY (section_id)
REFERENCES sections(section_id) ON UPDATE CASCADE
) ENGINE=InnoDB COMMENT='Student section enrollments';
-- ============================================================
-- TABLE: grades
-- ============================================================
CREATE TABLE IF NOT EXISTS grades (
grade_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
enrollment_id INT UNSIGNED NOT NULL,
numeric_grade DECIMAL(5,2) NULL COMMENT '0.00 - 100.00',
letter_grade CHAR(2) NULL,
grade_points DECIMAL(3,2) NULL COMMENT 'GPA points: 4.0 scale',
graded_at DATETIME NULL,
graded_by INT UNSIGNED NULL COMMENT 'FK -> instructors',
is_final TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (grade_id),
UNIQUE KEY uq_grade_enrollment (enrollment_id),
KEY idx_grade_instructor (graded_by),
CONSTRAINT fk_grade_enrollment FOREIGN KEY (enrollment_id)
REFERENCES enrollments(enrollment_id) ON DELETE CASCADE,
CONSTRAINT fk_grade_instructor FOREIGN KEY (graded_by)
REFERENCES instructors(instructor_id) ON DELETE SET NULL,
CONSTRAINT chk_numeric_grade CHECK (numeric_grade BETWEEN 0.00 AND 100.00)
) ENGINE=InnoDB COMMENT='Grades for enrollments';
-- ============================================================
-- TABLE: audit_log
-- ============================================================
CREATE TABLE IF NOT EXISTS audit_log (
log_id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
event_ts DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
event_type VARCHAR(40) NOT NULL,
table_name VARCHAR(64) NOT NULL,
record_id INT UNSIGNED NULL,
user_name VARCHAR(80) NOT NULL DEFAULT (USER()),
old_value JSON NULL,
new_value JSON NULL,
ip_address VARCHAR(45) NULL,
PRIMARY KEY (log_id),
KEY idx_audit_ts (event_ts),
KEY idx_audit_table (table_name, event_ts),
KEY idx_audit_user (user_name)
) ENGINE=InnoDB COMMENT='Immutable audit trail for sensitive changes';
SET FOREIGN_KEY_CHECKS = 1;
3. Advanced Stored Procedures¶
3.1 sp_enroll_student — With Prerequisites and Conflict Checking¶
DELIMITER $$
DROP PROCEDURE IF EXISTS sp_enroll_student$$
CREATE PROCEDURE sp_enroll_student(
IN p_student_id INT UNSIGNED,
IN p_section_id INT UNSIGNED,
OUT p_status_code TINYINT, -- 0=success, 1-n=error codes
OUT p_message VARCHAR(255)
)
sp_block: BEGIN
DECLARE v_enrolled_count TINYINT UNSIGNED DEFAULT 0;
DECLARE v_capacity TINYINT UNSIGNED DEFAULT 0;
DECLARE v_course_id INT UNSIGNED;
DECLARE v_semester VARCHAR(10);
DECLARE v_student_status VARCHAR(20);
DECLARE v_prereq_id INT UNSIGNED;
DECLARE v_prereq_met TINYINT DEFAULT 0;
DECLARE v_conflict_count INT DEFAULT 0;
DECLARE v_enrollment_id INT UNSIGNED;
DECLARE done INT DEFAULT FALSE;
-- Cursor: iterate all prerequisites for this course
DECLARE prereq_cur CURSOR FOR
SELECT p.prereq_id
FROM prerequisites p
WHERE p.course_id = v_course_id;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SET p_status_code = 99;
SET p_message = 'Unexpected database error — enrollment rolled back';
END;
-- ---- Step 1: Validate student is active -----------------
SELECT enrollment_status INTO v_student_status
FROM students
WHERE student_id = p_student_id;
IF v_student_status IS NULL THEN
SET p_status_code = 1;
SET p_message = 'Student not found';
LEAVE sp_block;
END IF;
IF v_student_status != 'active' THEN
SET p_status_code = 2;
SET p_message = CONCAT('Student status is ', v_student_status,
'; must be active to enroll');
LEAVE sp_block;
END IF;
-- ---- Step 2: Get section info ----------------------------
SELECT s.capacity, s.enrolled_count, s.course_id, s.semester
INTO v_capacity, v_enrolled_count, v_course_id, v_semester
FROM sections s
WHERE s.section_id = p_section_id
AND s.status IN ('open', 'waitlist')
FOR UPDATE;
IF v_course_id IS NULL THEN
SET p_status_code = 3;
SET p_message = 'Section not found or not open for enrollment';
LEAVE sp_block;
END IF;
-- ---- Step 3: Check capacity ------------------------------
IF v_enrolled_count >= v_capacity THEN
-- Add to waitlist instead
INSERT INTO enrollments (student_id, section_id, status)
VALUES (p_student_id, p_section_id, 'waitlisted')
ON DUPLICATE KEY UPDATE status = 'waitlisted';
SET p_status_code = 4;
SET p_message = 'Section full — added to waitlist';
LEAVE sp_block;
END IF;
-- ---- Step 4: Check prerequisites ------------------------
OPEN prereq_cur;
prereq_loop: LOOP
FETCH prereq_cur INTO v_prereq_id;
IF done THEN LEAVE prereq_loop; END IF;
SET v_prereq_met = 0;
SELECT COUNT(*) INTO v_prereq_met
FROM enrollments e
JOIN grades g ON g.enrollment_id = e.enrollment_id
JOIN sections s ON s.section_id = e.section_id
WHERE e.student_id = p_student_id
AND s.course_id = v_prereq_id
AND g.is_final = 1
AND g.letter_grade NOT IN ('F','W','I');
IF v_prereq_met = 0 THEN
CLOSE prereq_cur;
SET p_status_code = 5;
SET p_message = CONCAT('Prerequisite not satisfied: course_id = ',
v_prereq_id);
LEAVE sp_block;
END IF;
END LOOP prereq_loop;
CLOSE prereq_cur;
-- ---- Step 5: Check schedule conflicts -------------------
SELECT COUNT(*) INTO v_conflict_count
FROM enrollments e2
JOIN sections s2 ON s2.section_id = e2.section_id
JOIN sections s1 ON s1.section_id = p_section_id
WHERE e2.student_id = p_student_id
AND e2.status = 'enrolled'
AND s2.semester = v_semester
AND s2.section_id != p_section_id
AND FIND_IN_SET(SUBSTRING_INDEX(s2.schedule_days, ',', 1),
s1.schedule_days) > 0
AND s2.start_time < s1.end_time
AND s2.end_time > s1.start_time;
IF v_conflict_count > 0 THEN
SET p_status_code = 6;
SET p_message = 'Schedule conflict with an existing enrollment';
LEAVE sp_block;
END IF;
-- ---- Step 6: Perform enrollment -------------------------
START TRANSACTION;
INSERT INTO enrollments (student_id, section_id, status)
VALUES (p_student_id, p_section_id, 'enrolled')
ON DUPLICATE KEY UPDATE status = 'enrolled', drop_date = NULL;
SET v_enrollment_id = LAST_INSERT_ID();
UPDATE sections
SET enrolled_count = enrolled_count + 1
WHERE section_id = p_section_id;
-- Create placeholder grade record
INSERT IGNORE INTO grades (enrollment_id)
VALUES (v_enrollment_id);
COMMIT;
SET p_status_code = 0;
SET p_message = CONCAT('Successfully enrolled in section ', p_section_id);
END$$
DELIMITER ;
3.2 sp_calculate_gpa — Per Student, Per Semester¶
DELIMITER $$
DROP PROCEDURE IF EXISTS sp_calculate_gpa$$
CREATE PROCEDURE sp_calculate_gpa(
IN p_student_id INT UNSIGNED,
IN p_semester VARCHAR(10), -- NULL for cumulative
OUT p_gpa DECIMAL(4,3),
OUT p_credits SMALLINT UNSIGNED
)
BEGIN
SELECT
ROUND(
SUM(g.grade_points * c.credit_hours)
/ NULLIF(SUM(c.credit_hours), 0),
3) AS gpa,
SUM(c.credit_hours) AS total_credits
INTO p_gpa, p_credits
FROM enrollments e
JOIN grades g ON g.enrollment_id = e.enrollment_id
JOIN sections s ON s.section_id = e.section_id
JOIN courses c ON c.course_id = s.course_id
WHERE e.student_id = p_student_id
AND e.status IN ('enrolled', 'completed')
AND g.is_final = 1
AND g.grade_points IS NOT NULL
AND (p_semester IS NULL OR s.semester = p_semester);
IF p_gpa IS NULL THEN
SET p_gpa = 0.000;
SET p_credits = 0;
END IF;
-- Update students table if cumulative
IF p_semester IS NULL THEN
UPDATE students
SET gpa = p_gpa,
credit_hours = p_credits
WHERE student_id = p_student_id;
END IF;
END$$
DELIMITER ;
3.3 sp_generate_transcript — Cursor-Based¶
DELIMITER $$
DROP PROCEDURE IF EXISTS sp_generate_transcript$$
CREATE PROCEDURE sp_generate_transcript(
IN p_student_id INT UNSIGNED
)
BEGIN
DECLARE v_semester VARCHAR(10);
DECLARE v_course_code VARCHAR(10);
DECLARE v_title VARCHAR(120);
DECLARE v_credits TINYINT UNSIGNED;
DECLARE v_letter CHAR(2);
DECLARE v_points DECIMAL(3,2);
DECLARE v_sem_gpa DECIMAL(4,3);
DECLARE v_sem_credits SMALLINT;
DECLARE v_cum_points DECIMAL(8,3) DEFAULT 0;
DECLARE v_cum_credits SMALLINT DEFAULT 0;
DECLARE done INT DEFAULT FALSE;
-- Result set delivered via temp table
DROP TEMPORARY TABLE IF EXISTS tmp_transcript;
CREATE TEMPORARY TABLE tmp_transcript (
semester VARCHAR(10),
course_code VARCHAR(10),
course_title VARCHAR(120),
credit_hours TINYINT,
letter_grade CHAR(2),
grade_points DECIMAL(3,2),
sem_gpa DECIMAL(4,3),
cum_gpa DECIMAL(4,3)
);
DECLARE transcript_cur CURSOR FOR
SELECT s.semester, c.course_code, c.title,
c.credit_hours, g.letter_grade, g.grade_points
FROM enrollments e
JOIN sections s ON s.section_id = e.section_id
JOIN courses c ON c.course_id = s.course_id
JOIN grades g ON g.enrollment_id = e.enrollment_id
WHERE e.student_id = p_student_id
AND g.is_final = 1
ORDER BY s.semester, c.course_code;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN transcript_cur;
trans_loop: LOOP
FETCH transcript_cur INTO v_semester, v_course_code, v_title,
v_credits, v_letter, v_points;
IF done THEN LEAVE trans_loop; END IF;
SET v_cum_points = v_cum_points + (v_points * v_credits);
SET v_cum_credits = v_cum_credits + v_credits;
CALL sp_calculate_gpa(p_student_id, v_semester, v_sem_gpa, v_sem_credits);
INSERT INTO tmp_transcript VALUES (
v_semester, v_course_code, v_title, v_credits,
v_letter, v_points,
v_sem_gpa,
ROUND(v_cum_points / NULLIF(v_cum_credits, 0), 3)
);
END LOOP;
CLOSE transcript_cur;
SELECT * FROM tmp_transcript ORDER BY semester, course_code;
DROP TEMPORARY TABLE IF EXISTS tmp_transcript;
END$$
DELIMITER ;
4. Trigger Implementations¶
4.1 trg_enforce_capacity — BEFORE INSERT on enrollments¶
DELIMITER $$
DROP TRIGGER IF EXISTS trg_enforce_capacity$$
CREATE TRIGGER trg_enforce_capacity
BEFORE INSERT ON enrollments
FOR EACH ROW
BEGIN
DECLARE v_enrolled TINYINT UNSIGNED;
DECLARE v_capacity TINYINT UNSIGNED;
DECLARE v_status VARCHAR(10);
SELECT enrolled_count, capacity, status
INTO v_enrolled, v_capacity, v_status
FROM sections
WHERE section_id = NEW.section_id;
IF v_status = 'cancelled' THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Cannot enroll: section has been cancelled';
END IF;
IF v_enrolled >= v_capacity AND NEW.status = 'enrolled' THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Cannot enroll: section is at capacity';
END IF;
END$$
DELIMITER ;
4.2 trg_audit_grade_change — AFTER UPDATE on grades¶
DELIMITER $$
DROP TRIGGER IF EXISTS trg_audit_grade_change$$
CREATE TRIGGER trg_audit_grade_change
AFTER UPDATE ON grades
FOR EACH ROW
BEGIN
-- Only log when numeric_grade or letter_grade actually changes
IF (OLD.numeric_grade != NEW.numeric_grade
OR OLD.letter_grade != NEW.letter_grade
OR OLD.is_final != NEW.is_final)
THEN
INSERT INTO audit_log (
event_type, table_name, record_id,
user_name, old_value, new_value
)
VALUES (
'GRADE_CHANGE',
'grades',
NEW.grade_id,
USER(),
JSON_OBJECT(
'numeric_grade', OLD.numeric_grade,
'letter_grade', OLD.letter_grade,
'grade_points', OLD.grade_points,
'is_final', OLD.is_final
),
JSON_OBJECT(
'numeric_grade', NEW.numeric_grade,
'letter_grade', NEW.letter_grade,
'grade_points', NEW.grade_points,
'is_final', NEW.is_final,
'graded_by', NEW.graded_by
)
);
END IF;
END$$
DELIMITER ;
4.3 trg_update_enrollment_count — AFTER UPDATE on enrollments¶
DELIMITER $$
DROP TRIGGER IF EXISTS trg_update_enrollment_count$$
CREATE TRIGGER trg_update_enrollment_count
AFTER UPDATE ON enrollments
FOR EACH ROW
BEGIN
-- Student dropped: decrement count
IF OLD.status = 'enrolled' AND NEW.status IN ('dropped','withdrawn') THEN
UPDATE sections
SET enrolled_count = GREATEST(0, enrolled_count - 1),
status = CASE
WHEN enrolled_count - 1 < capacity THEN 'open'
ELSE status
END
WHERE section_id = NEW.section_id;
-- Student re-enrolled from waitlist or dropped state
ELSEIF OLD.status != 'enrolled' AND NEW.status = 'enrolled' THEN
UPDATE sections
SET enrolled_count = enrolled_count + 1,
status = CASE
WHEN enrolled_count + 1 >= capacity THEN 'closed'
ELSE 'open'
END
WHERE section_id = NEW.section_id;
END IF;
END$$
DELIMITER ;
5. User-Defined Functions¶
DELIMITER $$
-- ============================================================
-- fn_letter_grade: Convert numeric score to letter grade
-- ============================================================
DROP FUNCTION IF EXISTS fn_letter_grade$$
CREATE FUNCTION fn_letter_grade(p_score DECIMAL(5,2))
RETURNS CHAR(2)
DETERMINISTIC NO SQL
BEGIN
RETURN CASE
WHEN p_score >= 93 THEN 'A'
WHEN p_score >= 90 THEN 'A-'
WHEN p_score >= 87 THEN 'B+'
WHEN p_score >= 83 THEN 'B'
WHEN p_score >= 80 THEN 'B-'
WHEN p_score >= 77 THEN 'C+'
WHEN p_score >= 73 THEN 'C'
WHEN p_score >= 70 THEN 'C-'
WHEN p_score >= 67 THEN 'D+'
WHEN p_score >= 60 THEN 'D'
ELSE 'F'
END;
END$$
-- ============================================================
-- fn_student_gpa: Calculate current cumulative GPA
-- ============================================================
DROP FUNCTION IF EXISTS fn_student_gpa$$
CREATE FUNCTION fn_student_gpa(p_student_id INT UNSIGNED)
RETURNS DECIMAL(4,3)
READS SQL DATA
BEGIN
DECLARE v_gpa DECIMAL(4,3) DEFAULT 0.000;
SELECT ROUND(
SUM(g.grade_points * c.credit_hours)
/ NULLIF(SUM(c.credit_hours), 0),
3)
INTO v_gpa
FROM enrollments e
JOIN grades g ON g.enrollment_id = e.enrollment_id
JOIN sections s ON s.section_id = e.section_id
JOIN courses c ON c.course_id = s.course_id
WHERE e.student_id = p_student_id
AND g.is_final = 1
AND g.grade_points IS NOT NULL;
RETURN COALESCE(v_gpa, 0.000);
END$$
-- ============================================================
-- fn_credit_hours_completed: Credits earned in a semester
-- ============================================================
DROP FUNCTION IF EXISTS fn_credit_hours_completed$$
CREATE FUNCTION fn_credit_hours_completed(
p_student_id INT UNSIGNED,
p_semester VARCHAR(10)
)
RETURNS SMALLINT UNSIGNED
READS SQL DATA
BEGIN
DECLARE v_hours SMALLINT UNSIGNED DEFAULT 0;
SELECT COALESCE(SUM(c.credit_hours), 0) INTO v_hours
FROM enrollments e
JOIN sections s ON s.section_id = e.section_id
JOIN courses c ON c.course_id = s.course_id
JOIN grades g ON g.enrollment_id = e.enrollment_id
WHERE e.student_id = p_student_id
AND s.semester = p_semester
AND g.is_final = 1
AND g.letter_grade NOT IN ('F','W','I');
RETURN v_hours;
END$$
DELIMITER ;
6. Security Implementation¶
6.1 Role-Based Access Control¶
-- ============================================================
-- Step 1: Create Roles
-- ============================================================
CREATE ROLE IF NOT EXISTS student_portal;
CREATE ROLE IF NOT EXISTS faculty_role;
CREATE ROLE IF NOT EXISTS registrar_role;
CREATE ROLE IF NOT EXISTS admin_role;
-- ============================================================
-- Step 2: Grant Privileges to Roles
-- ============================================================
-- Student Portal: Read own data + enroll via procedure
GRANT SELECT ON fcrs.courses TO student_portal;
GRANT SELECT ON fcrs.sections TO student_portal;
GRANT SELECT ON fcrs.departments TO student_portal;
GRANT SELECT ON fcrs.prerequisites TO student_portal;
-- Students can only see their own rows via views (not direct table access)
GRANT SELECT ON fcrs.v_student_transcript TO student_portal;
GRANT SELECT ON fcrs.v_course_availability TO student_portal;
GRANT EXECUTE ON PROCEDURE fcrs.sp_enroll_student TO student_portal;
GRANT EXECUTE ON FUNCTION fcrs.fn_letter_grade TO student_portal;
-- Faculty Role: Grade entry + view own schedule
GRANT SELECT ON fcrs.courses TO faculty_role;
GRANT SELECT ON fcrs.sections TO faculty_role;
GRANT SELECT ON fcrs.enrollments TO faculty_role;
GRANT SELECT ON fcrs.students TO faculty_role;
GRANT SELECT, UPDATE (
numeric_grade, letter_grade, grade_points, graded_at, graded_by, is_final
) ON fcrs.grades TO faculty_role;
GRANT SELECT ON fcrs.v_faculty_schedule TO faculty_role;
GRANT EXECUTE ON PROCEDURE fcrs.sp_calculate_gpa TO faculty_role;
GRANT EXECUTE ON FUNCTION fcrs.fn_letter_grade TO faculty_role;
-- Registrar Role: Full data + enrollment management
GRANT SELECT, INSERT, UPDATE ON fcrs.students TO registrar_role;
GRANT SELECT, INSERT, UPDATE ON fcrs.enrollments TO registrar_role;
GRANT SELECT, INSERT, UPDATE ON fcrs.grades TO registrar_role;
GRANT SELECT, INSERT, UPDATE ON fcrs.sections TO registrar_role;
GRANT SELECT ON fcrs.audit_log TO registrar_role;
GRANT SELECT ON fcrs.v_student_transcript TO registrar_role;
GRANT SELECT ON fcrs.v_department_stats TO registrar_role;
GRANT EXECUTE ON PROCEDURE fcrs.sp_enroll_student TO registrar_role;
GRANT EXECUTE ON PROCEDURE fcrs.sp_generate_transcript TO registrar_role;
GRANT EXECUTE ON PROCEDURE fcrs.sp_calculate_gpa TO registrar_role;
-- Admin Role: Full schema access
GRANT ALL PRIVILEGES ON fcrs.* TO admin_role;
-- ============================================================
-- Step 3: Create Application Users and Assign Roles
-- ============================================================
CREATE USER IF NOT EXISTS 'portal_app'@'10.0.1.%'
IDENTIFIED WITH caching_sha2_password BY 'Portal@2025!Secure'
REQUIRE SSL
WITH MAX_USER_CONNECTIONS 200;
GRANT student_portal TO 'portal_app'@'10.0.1.%';
SET DEFAULT ROLE student_portal FOR 'portal_app'@'10.0.1.%';
CREATE USER IF NOT EXISTS 'faculty_app'@'10.0.1.%'
IDENTIFIED WITH caching_sha2_password BY 'Faculty@2025!Secure'
REQUIRE SSL;
GRANT faculty_role TO 'faculty_app'@'10.0.1.%';
SET DEFAULT ROLE faculty_role FOR 'faculty_app'@'10.0.1.%';
CREATE USER IF NOT EXISTS 'registrar_app'@'10.0.0.%'
IDENTIFIED WITH caching_sha2_password BY 'Registrar@2025!Secure'
REQUIRE SSL;
GRANT registrar_role TO 'registrar_app'@'10.0.0.%';
SET DEFAULT ROLE registrar_role FOR 'registrar_app'@'10.0.0.%';
7. View Definitions¶
-- ============================================================
-- v_student_transcript
-- ============================================================
CREATE OR REPLACE VIEW v_student_transcript AS
SELECT
s.student_id,
s.fsuid,
CONCAT(s.first_name, ' ', s.last_name) AS student_name,
sec.semester,
c.course_code,
c.title AS course_title,
c.credit_hours,
g.numeric_grade,
g.letter_grade,
g.grade_points,
g.is_final,
i.last_name AS instructor_last
FROM students s
JOIN enrollments e ON e.student_id = s.student_id
JOIN sections sec ON sec.section_id = e.section_id
JOIN courses c ON c.course_id = sec.course_id
LEFT JOIN grades g ON g.enrollment_id = e.enrollment_id
LEFT JOIN instructors i ON i.instructor_id = sec.instructor_id
WHERE e.status IN ('enrolled','completed');
-- ============================================================
-- v_course_availability
-- ============================================================
CREATE OR REPLACE VIEW v_course_availability AS
SELECT
sec.section_id,
c.course_code,
c.title,
c.credit_hours,
sec.semester,
sec.section_num,
sec.schedule_days,
SEC_TO_TIME(TIME_TO_SEC(sec.start_time)) AS start_time,
SEC_TO_TIME(TIME_TO_SEC(sec.end_time)) AS end_time,
sec.room,
sec.delivery_mode,
sec.capacity,
sec.enrolled_count,
sec.capacity - sec.enrolled_count AS seats_available,
sec.status,
CONCAT(i.first_name, ' ', i.last_name) AS instructor_name
FROM sections sec
JOIN courses c ON c.course_id = sec.course_id
LEFT JOIN instructors i ON i.instructor_id = sec.instructor_id
WHERE sec.status != 'cancelled';
-- ============================================================
-- v_faculty_schedule
-- ============================================================
CREATE OR REPLACE VIEW v_faculty_schedule AS
SELECT
i.instructor_id,
CONCAT(i.first_name, ' ', i.last_name) AS instructor_name,
i.email,
d.dept_name,
sec.semester,
c.course_code,
c.title,
sec.section_num,
sec.schedule_days,
sec.start_time,
sec.end_time,
sec.room,
sec.enrolled_count,
sec.capacity,
sec.delivery_mode
FROM instructors i
JOIN departments d ON d.dept_id = i.dept_id
JOIN sections sec ON sec.instructor_id = i.instructor_id
JOIN courses c ON c.course_id = sec.course_id
WHERE sec.status != 'cancelled';
-- ============================================================
-- v_department_stats
-- ============================================================
CREATE OR REPLACE VIEW v_department_stats AS
SELECT
d.dept_id,
d.dept_code,
d.dept_name,
COUNT(DISTINCT s.student_id) AS declared_majors,
COUNT(DISTINCT c.course_id) AS active_courses,
COUNT(DISTINCT i.instructor_id) AS instructors,
ROUND(AVG(s.gpa), 3) AS avg_student_gpa,
SUM(CASE WHEN s.enrollment_status = 'active' THEN 1 ELSE 0 END) AS active_students
FROM departments d
LEFT JOIN students s ON s.dept_id = d.dept_id
LEFT JOIN courses c ON c.dept_id = d.dept_id AND c.is_active = 1
LEFT JOIN instructors i ON i.dept_id = d.dept_id AND i.is_active = 1
GROUP BY d.dept_id, d.dept_code, d.dept_name;
8. Index Strategy¶
8.1 Index Design and Justification¶
Every index carries a cost (write overhead, storage). Each must be justified by a concrete query pattern:
-- INDEX 1: Enrollment lookup by student + status
-- Query: "Show all active enrollments for student 1001"
-- EXPLAIN without index: type=ALL (full scan on enrollments)
-- EXPLAIN with index: type=ref, rows=~8 (student's enrollments only)
CREATE INDEX idx_enroll_student_status ON enrollments (student_id, status);
-- INDEX 2: Section search by semester
-- Query: "Find all open sections in Fall2025"
-- Without: full scan of sections. With: range scan on semester column.
CREATE INDEX idx_section_semester_status ON sections (semester, status);
-- INDEX 3: Course search by department and level
-- Query: "List all 400-level ITEC courses"
-- Composite index matches both conditions in WHERE clause.
CREATE INDEX idx_course_dept_level ON courses (dept_id, level, is_active);
-- INDEX 4: Grade lookup for GPA calculation
-- Query: sp_calculate_gpa joins grades on enrollment_id; verify is_final
CREATE INDEX idx_grade_enrollment_final ON grades (enrollment_id, is_final);
-- INDEX 5: Audit log time-range queries
-- Query: "Show all grade changes in the last 30 days"
-- Already has idx_audit_ts in schema DDL — verify with EXPLAIN
EXPLAIN SELECT * FROM audit_log
WHERE event_type = 'GRADE_CHANGE'
AND event_ts >= NOW() - INTERVAL 30 DAY;
-- Uses idx_audit_table (table_name, event_ts) composite — efficient
-- INDEX 6: Full-text search on course titles and descriptions
-- Query: "Search courses containing 'database'"
ALTER TABLE courses ADD FULLTEXT INDEX ft_course_search (title, description);
-- ---- Verify index usage with EXPLAIN -----------------------
EXPLAIN
SELECT s.fsuid, s.first_name, s.last_name,
sec.semester, c.course_code, g.letter_grade
FROM enrollments e
JOIN students s ON s.student_id = e.student_id
JOIN sections sec ON sec.section_id = e.section_id
JOIN courses c ON c.course_id = sec.course_id
LEFT JOIN grades g ON g.enrollment_id = e.enrollment_id
WHERE e.student_id = 1001
AND e.status = 'enrolled'\G
-- Expected: ALL indexes used; no full scans; rows estimates < 50
9. Import/Export Scripts¶
9.1 Python CSV Enrollment Importer with Validation¶
#!/usr/bin/env python3
"""
import_enrollments.py
Import enrollment records from CSV into FCRS database.
Validates every row before inserting; reports errors with row numbers.
CSV format: student_fsuid,course_code,section_num,semester
"""
import csv
import sys
import logging
import mysql.connector
from mysql.connector import Error
from pathlib import Path
from dataclasses import dataclass, field
from datetime import datetime
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
handlers=[
logging.FileHandler(f"import_{datetime.now():%Y%m%d_%H%M%S}.log"),
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
DB_CONFIG = {
"host": "127.0.0.1",
"database": "fcrs",
"user": "import_user",
"password": "import_pass",
"charset": "utf8mb4",
"autocommit": False,
}
@dataclass
class ImportStats:
total: int = 0
success: int = 0
skipped: int = 0
errors: list = field(default_factory=list)
def validate_row(row: dict, row_num: int) -> list[str]:
"""Return list of validation error messages (empty = valid)."""
errors = []
if not row.get("student_fsuid", "").strip():
errors.append(f"Row {row_num}: missing student_fsuid")
elif not row["student_fsuid"].startswith("B") or len(row["student_fsuid"]) != 9:
errors.append(f"Row {row_num}: invalid FSUID format '{row['student_fsuid']}'")
if not row.get("course_code", "").strip():
errors.append(f"Row {row_num}: missing course_code")
if not row.get("section_num", "").strip().isdigit():
errors.append(f"Row {row_num}: section_num must be numeric")
semester = row.get("semester", "").strip()
valid_terms = ("Fall", "Spring", "Summer")
if not any(semester.startswith(t) for t in valid_terms):
errors.append(f"Row {row_num}: invalid semester '{semester}'")
return errors
def import_csv(filepath: str) -> ImportStats:
stats = ImportStats()
conn = None
try:
conn = mysql.connector.connect(**DB_CONFIG)
cursor = conn.cursor(dictionary=True)
# Lookup helpers
cursor.execute("SELECT student_id, fsuid FROM students")
fsuid_map = {r["fsuid"]: r["student_id"] for r in cursor.fetchall()}
cursor.execute("""
SELECT sec.section_id, c.course_code, sec.section_num, sec.semester
FROM sections sec JOIN courses c ON c.course_id = sec.course_id
""")
section_map = {
(r["course_code"], str(r["section_num"]), r["semester"]): r["section_id"]
for r in cursor.fetchall()
}
with open(filepath, newline="", encoding="utf-8-sig") as f:
reader = csv.DictReader(f)
for row_num, row in enumerate(reader, start=2):
stats.total += 1
# Validation
row_errors = validate_row(row, row_num)
if row_errors:
stats.errors.extend(row_errors)
stats.skipped += 1
continue
fsuid = row["student_fsuid"].strip()
course_code = row["course_code"].strip().upper()
section_num = row["section_num"].strip()
semester = row["semester"].strip()
student_id = fsuid_map.get(fsuid)
if not student_id:
stats.errors.append(f"Row {row_num}: student not found: {fsuid}")
stats.skipped += 1
continue
section_id = section_map.get((course_code, section_num, semester))
if not section_id:
stats.errors.append(
f"Row {row_num}: section not found: "
f"{course_code} S{section_num} {semester}"
)
stats.skipped += 1
continue
# Call stored procedure
try:
cursor.callproc("sp_enroll_student",
[student_id, section_id, 0, ""])
for res in cursor.stored_results():
pass # consume result sets
conn.commit()
stats.success += 1
logger.info("Enrolled %s in %s S%s %s",
fsuid, course_code, section_num, semester)
except Error as exc:
conn.rollback()
stats.errors.append(f"Row {row_num}: DB error — {exc}")
stats.skipped += 1
except Error as exc:
logger.error("Connection error: %s", exc)
sys.exit(1)
finally:
if conn and conn.is_connected():
cursor.close()
conn.close()
return stats
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python import_enrollments.py <enrollments.csv>")
sys.exit(1)
csv_path = sys.argv[1]
if not Path(csv_path).exists():
logger.error("File not found: %s", csv_path)
sys.exit(1)
stats = import_csv(csv_path)
logger.info("=" * 50)
logger.info("Import complete: %d total, %d enrolled, %d skipped",
stats.total, stats.success, stats.skipped)
if stats.errors:
logger.warning("Errors encountered:")
for err in stats.errors:
logger.warning(" %s", err)
sys.exit(0 if not stats.errors else 1)
9.2 Automated mysqldump Backup Script¶
#!/usr/bin/env bash
set -euo pipefail
readonly DB_NAME="fcrs"
readonly BACKUP_DIR="/var/backups/mysql/fcrs"
readonly TS=$(date +%Y%m%d_%H%M%S)
readonly FILE="${BACKUP_DIR}/fcrs_${TS}.sql.gz"
mkdir -p "$BACKUP_DIR"
mysqldump \
--defaults-file=/etc/mysql/backup.cnf \
--single-transaction --flush-logs --master-data=2 \
--routines --triggers --events --add-drop-table \
"$DB_NAME" | gzip -9 > "$FILE"
echo "Backup: $FILE ($(du -sh "$FILE" | cut -f1))"
# Keep 7 days
ls -1t "${BACKUP_DIR}/"*.sql.gz | tail -n +8 | xargs -r rm -f
10. Event Scheduler — Nightly and Semester-End Automation¶
DELIMITER $$
-- ============================================================
-- e_nightly_waitlist_processing
-- Promotes waitlisted students if a seat opened (due to drops)
-- ============================================================
DROP EVENT IF EXISTS e_nightly_waitlist_processing$$
CREATE EVENT e_nightly_waitlist_processing
ON SCHEDULE EVERY 1 DAY
STARTS TIMESTAMP(CURRENT_DATE, '23:00:00')
ON COMPLETION PRESERVE ENABLE
DO
BEGIN
DECLARE v_student_id INT UNSIGNED;
DECLARE v_section_id INT UNSIGNED;
DECLARE v_enroll_id INT UNSIGNED;
DECLARE v_seats_free INT DEFAULT 0;
DECLARE done INT DEFAULT FALSE;
DECLARE waitlist_cur CURSOR FOR
SELECT e.student_id, e.section_id, e.enrollment_id
FROM enrollments e
JOIN sections s ON s.section_id = e.section_id
WHERE e.status = 'waitlisted'
AND s.enrolled_count < s.capacity
ORDER BY e.enrolled_at -- FIFO
LIMIT 100;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN waitlist_cur;
wl_loop: LOOP
FETCH waitlist_cur INTO v_student_id, v_section_id, v_enroll_id;
IF done THEN LEAVE wl_loop; END IF;
UPDATE enrollments
SET status = 'enrolled'
WHERE enrollment_id = v_enroll_id;
INSERT INTO audit_log (event_type, table_name, record_id, user_name)
VALUES ('WAITLIST_PROMOTED', 'enrollments', v_enroll_id, 'EVENT_SCHEDULER');
END LOOP;
CLOSE waitlist_cur;
END$$
-- ============================================================
-- e_semester_end_grade_finalization
-- Runs at end of each semester to finalise GPA calculations
-- ============================================================
DROP EVENT IF EXISTS e_semester_end_grade_finalization$$
CREATE EVENT e_semester_end_grade_finalization
ON SCHEDULE
AT '2025-12-20 06:00:00'
ON COMPLETION PRESERVE ENABLE
DO
BEGIN
DECLARE v_student_id INT UNSIGNED;
DECLARE done INT DEFAULT FALSE;
DECLARE student_cur CURSOR FOR
SELECT DISTINCT e.student_id
FROM enrollments e
JOIN sections s ON s.section_id = e.section_id
WHERE s.semester = 'Fall2025'
AND e.status = 'enrolled';
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN student_cur;
stu_loop: LOOP
FETCH student_cur INTO v_student_id;
IF done THEN LEAVE stu_loop; END IF;
-- Update enrollment status to completed
UPDATE enrollments e
JOIN sections s ON s.section_id = e.section_id
SET e.status = 'completed'
WHERE e.student_id = v_student_id
AND s.semester = 'Fall2025'
AND e.status = 'enrolled';
-- Recalculate GPA
UPDATE students
SET gpa = fn_student_gpa(v_student_id)
WHERE student_id = v_student_id;
END LOOP;
CLOSE student_cur;
INSERT INTO audit_log (event_type, table_name, user_name)
VALUES ('SEMESTER_END_FINALIZATION', 'enrollments', 'EVENT_SCHEDULER');
END$$
DELIMITER ;
11. Monitoring — Health Check Stored Procedure¶
DELIMITER $$
DROP PROCEDURE IF EXISTS sp_database_health_check$$
CREATE PROCEDURE sp_database_health_check()
BEGIN
-- ---- 1. Buffer pool hit rate ---------------------------
SELECT 'Buffer Pool Hit Rate %' AS metric,
ROUND(100 * (1 - (
(SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads')
/
NULLIF((SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests'), 0)
)), 2) AS value;
-- ---- 2. Active connections vs limit ---------------------
SELECT 'Connections Used %' AS metric,
ROUND(100 *
(SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Threads_connected')
/
NULLIF(@@max_connections, 0),
1) AS value;
-- ---- 3. Slow queries (last hour) -----------------------
SELECT 'Slow Queries (last 1hr)' AS metric,
COUNT(*) AS value
FROM performance_schema.events_statements_history_long
WHERE TIMER_WAIT > 1000000000000; -- > 1 second in picoseconds
-- ---- 4. Long-running transactions ----------------------
SELECT 'Long Transactions (> 5min)' AS metric,
COUNT(*) AS value
FROM information_schema.INNODB_TRX
WHERE trx_started < NOW() - INTERVAL 5 MINUTE;
-- ---- 5. Replication lag (if replica) -------------------
SELECT 'Replication Lag (s)' AS metric,
IFNULL(
(SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Seconds_Behind_Source'),
'N/A (not a replica)'
) AS value;
-- ---- 6. Table fragmentation ----------------------------
SELECT 'Fragmented Tables (>10% free)' AS metric,
COUNT(*) AS value
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'fcrs'
AND DATA_FREE > DATA_LENGTH * 0.10
AND ENGINE = 'InnoDB';
-- ---- 7. Deadlock count (since last restart) ------------
SELECT 'Deadlocks Since Restart' AS metric,
VARIABLE_VALUE AS value
FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Innodb_deadlocks';
END$$
DELIMITER ;
-- Run the health check
CALL sp_database_health_check();
12. Project Rubric¶
| Criterion | Weight | Excellent (90–100%) | Good (75–89%) | Needs Work (< 75%) |
|---|---|---|---|---|
| Schema Design | 20% | All 8+ tables; correct PK/FK; appropriate data types; CHECK constraints; 3NF verified | 6–7 tables; minor FK omissions; data types mostly correct | < 6 tables; missing foreign keys; redundant data; denormalisation without justification |
| Stored Procedures | 20% | All 3 procedures; cursor iteration; error handling with SIGNAL; transaction management; edge cases tested | 2 procedures; basic error handling; transactions used | 1 procedure; no error handling; no transactions |
| Triggers | 15% | All 3 triggers; BEFORE and AFTER; correct SIGNAL in BEFORE; JSON audit log in AFTER | 2 triggers; correct logic; minor audit gaps | 1 trigger or incorrect trigger logic |
| Functions | 10% | All 3 functions; DETERMINISTIC declared correctly; used in views and procedures | 2 functions; used independently | 1 function; not integrated |
| Security | 10% | 4 roles; granular per-column GRANT; SSL required; MAX_USER_CONNECTIONS; DEFINER clauses reviewed | 3 roles; mostly correct grants; SSL optional | < 3 roles; overly broad grants (e.g., ALL on schema) |
| Views | 10% | All 4 views; used in security layer; tested with JOIN performance; no SELECT * | 3 views; functionally correct | < 3 views or views expose raw tables |
| Indexing | 10% | Every index justified with EXPLAIN output; no redundant indexes; full-text where appropriate | Indexes present; some unjustified; EXPLAIN used selectively | Indexes missing on FK columns; no EXPLAIN analysis |
| Scripting & Automation | 5% | Python importer with row-level validation; error report; backup script with retention; events tested | Python importer runs; backup script works; events defined | Import script only; no validation; no automation |
| Documentation | ___ | Schema diagram; procedure descriptions; security matrix; test queries with expected output | Schema diagram and key descriptions | Minimal or missing documentation |
13. Final Synthesis — Connecting 15 Weeks¶
13.1 The Complete Picture¶
Every topic in ITEC 445 forms one layer of a production database system. Here is how they connect:
WEEK 1-2: Relational theory, normalisation, ER modelling
↓ Foundation for all schema design decisions
WEEK 3-4: Advanced SQL — JOINs, subqueries, window functions
↓ Powers views, stored procedures, reporting
WEEK 5-6: Stored procedures, functions, cursors, error handling
↓ Encapsulates business logic in the database
WEEK 7-8: Triggers, events
↓ Automated enforcement and audit trail
WEEK 9-10: Security — users, roles, privileges, SSL
↓ Protects data; enables least-privilege access
WEEK 11: Scripting & Automation
↓ Bridges DB and OS; enables DevOps practices
WEEK 12: Backup, Recovery, Replication, HA
↓ Ensures data durability and service continuity
WEEK 13: Monitoring & Performance Tuning
↓ Keeps the system fast and observable in production
WEEK 14: NoSQL, NewSQL, Cloud
↓ Enables right-tool-for-right-job architecture
WEEK 15: Capstone — integrates everything into a real system
13.2 Career Paths in Database Engineering¶
Database Administrator (DBA)
The DBA owns the health, performance, security, and availability of the database server itself. This is the most operationally intensive database role.
| Skill Area | ITEC 445 Coverage |
|---|---|
| Backup & Recovery | Week 12 — mysqldump, XtraBackup, PITR |
| Replication & HA | Week 12 — GTID replication, InnoDB Cluster |
| Performance Tuning | Week 13 — Performance Schema, buffer pool, deadlocks |
| Security | Weeks 9–10, Week 15 — roles, SSL, audit |
| Automation | Week 11 — scripting, Event Scheduler |
Typical tools: MySQL, Percona XtraBackup, PMM, ProxySQL, Ansible
Entry salary range (US): $75,000–$110,000
Database Developer / SQL Developer
Focused on the application-facing database layer: schema design, stored procedures, views, and query optimisation.
| Skill Area | ITEC 445 Coverage |
|---|---|
| Schema Design | Weeks 1–2, Week 15 capstone |
| Stored Procedures | Weeks 5–6, Week 15 |
| Triggers & Events | Weeks 7–8, Week 15 |
| Indexing | Weeks 9–10, Week 15 |
| Migration Tooling | Week 11 — Flyway, Liquibase |
Typical tools: MySQL Workbench, DBeaver, Git, Flyway, Visual Studio Code
Entry salary range (US): $70,000–$100,000
Data Engineer
Builds and maintains the pipelines that move, transform, and land data for analytics and machine learning.
| Skill Area | ITEC 445 Coverage |
|---|---|
| Python + SQL scripting | Week 11 — mysql-connector, SQLAlchemy |
| ETL / CSV import | Week 15 — Python importer with validation |
| Cloud databases | Week 14 — AWS, GCP, Azure |
| NoSQL systems | Week 14 — MongoDB, Redis |
| Schema modelling | Weeks 1–2 (relational) + Week 14 (document) |
Typical tools: Python, dbt, Apache Airflow, Spark, Kafka, Redshift
Entry salary range (US): $85,000–$125,000
Cloud Database Engineer / Site Reliability Engineer (SRE)
Operates database infrastructure at cloud scale; combines DBA skills with DevOps and infrastructure-as-code expertise.
| Skill Area | ITEC 445 Coverage |
|---|---|
| Cloud managed databases | Week 14 — RDS, Aurora, DynamoDB, Cloud SQL |
| HA & failover | Week 12 — InnoDB Cluster, Multi-AZ |
| Monitoring & alerting | Week 11 (health scripts), Week 13 (PMM, Prometheus) |
| Automation & scripting | Week 11 — bash, Python, event scheduler |
| Performance | Week 13 — tuning, OS-level optimisation |
Typical tools: AWS/GCP/Azure, Terraform, Ansible, Prometheus, Grafana, PagerDuty
Entry salary range (US): $90,000–$135,000
Career Advice from the Database World
The most impactful database engineers are those who can communicate across boundaries — they speak SQL to developers, explain I/O patterns to sysadmins, and translate business requirements into schema decisions. Your SQL is a tool; your judgment is the skill.
Key Vocabulary¶
| Term | Definition |
|---|---|
| Capstone | Integrative final project demonstrating mastery of all course objectives |
| Normalisation | Formal process of organising schema to reduce redundancy and dependency anomalies |
SIGNAL SQLSTATE | MySQL statement to raise a user-defined error from a stored procedure or trigger |
LEAVE | MySQL statement to exit a labelled loop or BEGIN…END block |
| Cursor | Server-side iterator over a result set, used in stored procedures |
| BEFORE trigger | Executes before the DML statement; can modify NEW row or cancel via SIGNAL |
| AFTER trigger | Executes after the DML statement succeeds; used for auditing side effects |
DETERMINISTIC | Function attribute declaring output depends only on inputs (same inputs → same output) |
READS SQL DATA | Function attribute declaring it reads data but does not modify it |
| Role | Named collection of privileges that can be assigned to users |
REQUIRE SSL | User creation clause requiring encrypted connections |
DEFINER | MySQL user whose security context is used when executing a stored object |
| Generated column | Column whose value is computed from an expression, optionally indexed |
FULLTEXT index | Specialised MySQL index for efficient text search on VARCHAR/TEXT columns |
ON COMPLETION PRESERVE | Event clause keeping a one-time event after it fires |
| ETL | Extract, Transform, Load — process of moving data between systems |
arrayFilters | MongoDB update option for matching specific elements in an array |
| RPO | Recovery Point Objective — maximum acceptable data loss in time |
| RTO | Recovery Time Objective — maximum acceptable time to restore service |
| Polyglot persistence | Using multiple specialised databases within one application architecture |
Self-Assessment¶
Self-Assessment — Week 15 (Capstone)
Question 1. The trg_enforce_capacity trigger raises a SIGNAL to block an INSERT when the section is full. However, sp_enroll_student also checks capacity before inserting. Explain why both checks are necessary, and describe a race condition that the stored procedure check alone (without the trigger) would fail to prevent in a concurrent environment.
Question 2. Review the sp_generate_transcript procedure. It calls sp_calculate_gpa inside a cursor loop — one call per semester. Identify the N+1 query problem this creates, estimate the performance impact for a student with 8 semesters, and rewrite the relevant section to eliminate the N+1 calls without changing the procedure's output.
Question 3. The registrar_role is granted SELECT, INSERT, UPDATE on enrollments directly (not only via the stored procedure). Explain the security implication of this direct table grant, design a tighter privilege model that preserves registrar functionality without direct DML on enrollments, and describe how SQL SECURITY DEFINER on the stored procedure supports this model.
Question 4. Your EXPLAIN output for the enrollment search query shows type=ALL on the grades table despite the idx_grade_enrollment_final composite index existing. List three reasons MySQL might ignore a composite index, and describe how you would diagnose which reason applies in this case.
Question 5. You are presenting the FCRS architecture to the FSU IT steering committee. A committee member asks: "Why not just put all this data in MongoDB? It's more flexible." Write a 4–6 sentence technical argument for the relational model in the context of a course registration system, acknowledging one specific use case where adding MongoDB (in a polyglot architecture) would be justified.
Further Reading¶
- MySQL 8.0 Stored Programs
- MySQL 8.0 Triggers
- MySQL 8.0 Role-Based Access Control
- Percona Toolkit Documentation
- Use The Index, Luke — SQL Indexing for Developers
- Schwartz, Baron et al. High Performance MySQL, 4th Ed. — Chapter 6 (Schema Design)
- Kleppmann, Martin. Designing Data-Intensive Applications — Chapters 1–2 (Foundations)
- Database Reliability Engineering (O'Reilly)
- MySQL Certification Study Guide
A Closing Note to Students¶
You have spent fifteen weeks building skills that most developers spend years accumulating through trial and error in production. You understand not just how to write SQL, but why an index makes a query fast, what happens inside InnoDB when you commit a transaction, and how to keep a database running reliably at 3 AM when things go wrong.
The database is the memory of every application that matters — the place where a hospital stores patient records, where a university records a student's achievement, where a business processes its transactions. The work of a database engineer is quiet, unglamorous, and absolutely essential.
Carry that responsibility seriously. Write code that future colleagues can read. Design schemas that survive requirements changes. Build systems that fail gracefully. And always, always test your backups.
Good luck on your capstone projects. — Dr. Chen