package database import ( "context" "log" "time" "github.com/jackc/pgx/v5/pgxpool" ) // DB wraps the database connection pool type DB struct { Pool *pgxpool.Pool } // Connect creates a new database connection pool func Connect(databaseURL string) (*DB, error) { config, err := pgxpool.ParseConfig(databaseURL) if err != nil { return nil, err } // Configure connection pool config.MaxConns = 25 config.MinConns = 5 config.MaxConnLifetime = time.Hour config.MaxConnIdleTime = 30 * time.Minute config.HealthCheckPeriod = time.Minute pool, err := pgxpool.NewWithConfig(context.Background(), config) if err != nil { return nil, err } // Test connection if err := pool.Ping(context.Background()); err != nil { return nil, err } log.Println("Database connection established") return &DB{Pool: pool}, nil } // Close closes the database connection pool func (db *DB) Close() { db.Pool.Close() } // Migrate runs database migrations func Migrate(db *DB) error { ctx := context.Background() migrations := []string{ // School Years `CREATE TABLE IF NOT EXISTS school_years ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(20) NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, is_current BOOLEAN DEFAULT false, teacher_id UUID NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() )`, // Classes `CREATE TABLE IF NOT EXISTS classes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), teacher_id UUID NOT NULL, school_year_id UUID REFERENCES school_years(id), name VARCHAR(20) NOT NULL, grade_level INT NOT NULL, school_type VARCHAR(30), federal_state VARCHAR(50), created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(teacher_id, school_year_id, name) )`, // Students `CREATE TABLE IF NOT EXISTS students ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), class_id UUID REFERENCES classes(id) ON DELETE CASCADE, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, birth_date DATE, student_number VARCHAR(50), notes TEXT, created_at TIMESTAMPTZ DEFAULT NOW() )`, // Subjects `CREATE TABLE IF NOT EXISTS subjects ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), teacher_id UUID NOT NULL, name VARCHAR(100) NOT NULL, short_name VARCHAR(10), is_main_subject BOOLEAN DEFAULT false, created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(teacher_id, name) )`, // Exams `CREATE TABLE IF NOT EXISTS exams ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), teacher_id UUID NOT NULL, class_id UUID REFERENCES classes(id), subject_id UUID REFERENCES subjects(id), title VARCHAR(255) NOT NULL, exam_type VARCHAR(30) NOT NULL, topic VARCHAR(255), content TEXT, source_file_path VARCHAR(500), difficulty_level INT DEFAULT 3, duration_minutes INT, max_points DECIMAL(5,2), is_template BOOLEAN DEFAULT false, parent_exam_id UUID REFERENCES exams(id), status VARCHAR(20) DEFAULT 'draft', exam_date DATE, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() )`, // Exam Results `CREATE TABLE IF NOT EXISTS exam_results ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), exam_id UUID REFERENCES exams(id) ON DELETE CASCADE, student_id UUID REFERENCES students(id) ON DELETE CASCADE, points_achieved DECIMAL(5,2), grade DECIMAL(2,1), percentage DECIMAL(5,2), notes TEXT, is_absent BOOLEAN DEFAULT false, needs_rewrite BOOLEAN DEFAULT false, approved_by_teacher BOOLEAN DEFAULT false, approved_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(exam_id, student_id) )`, // Grade Overview `CREATE TABLE IF NOT EXISTS grade_overview ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), student_id UUID REFERENCES students(id) ON DELETE CASCADE, subject_id UUID REFERENCES subjects(id), school_year_id UUID REFERENCES school_years(id), semester INT NOT NULL, written_grade_avg DECIMAL(3,2), written_grade_count INT DEFAULT 0, oral_grade DECIMAL(3,2), oral_notes TEXT, final_grade DECIMAL(3,2), final_grade_locked BOOLEAN DEFAULT false, written_weight INT DEFAULT 60, oral_weight INT DEFAULT 40, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(student_id, subject_id, school_year_id, semester) )`, // Attendance `CREATE TABLE IF NOT EXISTS attendance ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), student_id UUID REFERENCES students(id) ON DELETE CASCADE, date DATE NOT NULL, status VARCHAR(20) NOT NULL, periods INT DEFAULT 1, reason TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(student_id, date) )`, // Gradebook Entries `CREATE TABLE IF NOT EXISTS gradebook_entries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), class_id UUID REFERENCES classes(id) ON DELETE CASCADE, student_id UUID REFERENCES students(id), date DATE NOT NULL, entry_type VARCHAR(30) NOT NULL, content TEXT NOT NULL, is_visible_to_parents BOOLEAN DEFAULT false, created_at TIMESTAMPTZ DEFAULT NOW() )`, // Certificates `CREATE TABLE IF NOT EXISTS certificates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), student_id UUID REFERENCES students(id) ON DELETE CASCADE, school_year_id UUID REFERENCES school_years(id), semester INT NOT NULL, certificate_type VARCHAR(30), template_name VARCHAR(100), grades_json JSONB, remarks TEXT, absence_days INT DEFAULT 0, absence_days_unexcused INT DEFAULT 0, generated_pdf_path VARCHAR(500), status VARCHAR(20) DEFAULT 'draft', created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() )`, // Indexes `CREATE INDEX IF NOT EXISTS idx_students_class ON students(class_id)`, `CREATE INDEX IF NOT EXISTS idx_exams_teacher ON exams(teacher_id)`, `CREATE INDEX IF NOT EXISTS idx_exams_class ON exams(class_id)`, `CREATE INDEX IF NOT EXISTS idx_exam_results_exam ON exam_results(exam_id)`, `CREATE INDEX IF NOT EXISTS idx_exam_results_student ON exam_results(student_id)`, `CREATE INDEX IF NOT EXISTS idx_grade_overview_student ON grade_overview(student_id)`, `CREATE INDEX IF NOT EXISTS idx_attendance_student ON attendance(student_id)`, `CREATE INDEX IF NOT EXISTS idx_attendance_date ON attendance(date)`, `CREATE INDEX IF NOT EXISTS idx_gradebook_class ON gradebook_entries(class_id)`, } for _, migration := range migrations { _, err := db.Pool.Exec(ctx, migration) if err != nil { log.Printf("Migration error: %v\nSQL: %s", err, migration) return err } } log.Println("Database migrations completed") return nil }