-- Students can belong to multiple classes: add StudentClass junction table and remove Student.classId -- 1. Create junction table CREATE TABLE "StudentClass" ( "studentId" TEXT NOT NULL REFERENCES "Student"("id") ON DELETE CASCADE, "classId" INTEGER NOT NULL REFERENCES "Class"("id") ON DELETE CASCADE, CONSTRAINT "StudentClass_pkey" PRIMARY KEY ("studentId", "classId") ); CREATE INDEX "StudentClass_classId_idx" ON "StudentClass"("classId"); CREATE INDEX "StudentClass_studentId_idx" ON "StudentClass"("studentId"); -- 2. Backfill from existing Student.classId INSERT INTO "StudentClass" ("studentId", "classId") SELECT id, "classId" FROM "Student" WHERE "classId" IS NOT NULL; -- 3. Drop FK and column on Student ALTER TABLE "Student" DROP CONSTRAINT IF EXISTS "Student_classId_fkey"; ALTER TABLE "Student" DROP COLUMN IF EXISTS "classId"; -- 4. RLS for StudentClass ALTER TABLE "StudentClass" ENABLE ROW LEVEL SECURITY; CREATE POLICY "Admins have full access" ON "StudentClass" FOR ALL USING ( EXISTS (SELECT 1 FROM "Admin" WHERE id = requesting_user_id()) ); CREATE POLICY "Users can view permitted student-class links" ON "StudentClass" FOR SELECT USING ( is_admin() OR ("studentId" = requesting_user_id() AND requesting_user_role() = 'student') OR ("studentId" IN (SELECT id FROM "Student" WHERE "parentId" = requesting_user_id()) AND requesting_user_role() = 'parent') OR ("classId" IN (SELECT "classId" FROM "Lesson" WHERE "teacherId" = requesting_user_id()) AND requesting_user_role() = 'teacher') OR ("classId" IN (SELECT id FROM "Class" WHERE "supervisorId" = requesting_user_id()) AND requesting_user_role() = 'teacher') ); CREATE POLICY "Admins can insert student-class" ON "StudentClass" FOR INSERT WITH CHECK (is_admin()); CREATE POLICY "Admins can update student-class" ON "StudentClass" FOR UPDATE USING (is_admin()); CREATE POLICY "Admins can delete student-class" ON "StudentClass" FOR DELETE USING (is_admin()); -- 5. Update auth helper: classes the user can see (student/parent via StudentClass, teacher via Lesson/Class) CREATE OR REPLACE FUNCTION auth_user_classes() RETURNS SETOF integer LANGUAGE plpgsql SECURITY DEFINER SET search_path = public STABLE AS $$ BEGIN RETURN QUERY SELECT "classId" FROM "StudentClass" WHERE "studentId" = requesting_user_id() AND requesting_user_role() = 'student' UNION SELECT "classId" FROM "StudentClass" WHERE "studentId" IN (SELECT id FROM "Student" WHERE "parentId" = requesting_user_id()) AND requesting_user_role() = 'parent' UNION SELECT "classId" FROM "Lesson" WHERE "teacherId" = requesting_user_id() AND requesting_user_role() = 'teacher' UNION SELECT id FROM "Class" WHERE "supervisorId" = requesting_user_id() AND requesting_user_role() = 'teacher'; END; $$; -- 6. Update auth_user_students: teacher sees students in their classes via StudentClass CREATE OR REPLACE FUNCTION auth_user_students() RETURNS SETOF text LANGUAGE plpgsql SECURITY DEFINER SET search_path = public STABLE AS $$ BEGIN RETURN QUERY SELECT id FROM "Student" WHERE id = requesting_user_id() AND requesting_user_role() = 'student' UNION SELECT id FROM "Student" WHERE "parentId" = requesting_user_id() AND requesting_user_role() = 'parent' UNION SELECT "studentId" FROM "StudentClass" WHERE "classId" IN ( SELECT "classId" FROM "Lesson" WHERE "teacherId" = requesting_user_id() ) AND requesting_user_role() = 'teacher'; END; $$; -- 7. Update auth_user_lessons: student/parent see lessons for any of their classes (via StudentClass) CREATE OR REPLACE FUNCTION auth_user_lessons() RETURNS SETOF integer LANGUAGE plpgsql SECURITY DEFINER SET search_path = public STABLE AS $$ BEGIN RETURN QUERY SELECT id FROM "Lesson" WHERE "teacherId" = requesting_user_id() AND requesting_user_role() = 'teacher' UNION SELECT id FROM "Lesson" WHERE "classId" IN ( SELECT "classId" FROM "StudentClass" WHERE "studentId" = requesting_user_id() AND requesting_user_role() = 'student' ) UNION SELECT id FROM "Lesson" WHERE "classId" IN ( SELECT "classId" FROM "StudentClass" WHERE "studentId" IN (SELECT id FROM "Student" WHERE "parentId" = requesting_user_id()) AND requesting_user_role() = 'parent' ); END; $$; -- 8. Update auth_user_subjects CREATE OR REPLACE FUNCTION auth_user_subjects() RETURNS SETOF integer LANGUAGE plpgsql SECURITY DEFINER SET search_path = public STABLE AS $$ BEGIN RETURN QUERY SELECT "A" FROM "_SubjectToTeacher" WHERE "B" = requesting_user_id() AND requesting_user_role() = 'teacher' UNION SELECT "subjectId" FROM "Lesson" WHERE "classId" IN ( SELECT "classId" FROM "StudentClass" WHERE "studentId" = requesting_user_id() AND requesting_user_role() = 'student' ) UNION SELECT "subjectId" FROM "Lesson" WHERE "classId" IN ( SELECT "classId" FROM "StudentClass" WHERE "studentId" IN (SELECT id FROM "Student" WHERE "parentId" = requesting_user_id()) AND requesting_user_role() = 'parent' ); END; $$; -- 9. Update auth_user_teachers CREATE OR REPLACE FUNCTION auth_user_teachers() RETURNS SETOF text LANGUAGE plpgsql SECURITY DEFINER SET search_path = public STABLE AS $$ BEGIN RETURN QUERY SELECT id FROM "Teacher" WHERE id = requesting_user_id() AND requesting_user_role() = 'teacher' UNION SELECT "teacherId" FROM "Lesson" WHERE "classId" IN ( SELECT "classId" FROM "StudentClass" WHERE "studentId" = requesting_user_id() AND requesting_user_role() = 'student' ) UNION SELECT "teacherId" FROM "Lesson" WHERE "classId" IN ( SELECT "classId" FROM "StudentClass" WHERE "studentId" IN (SELECT id FROM "Student" WHERE "parentId" = requesting_user_id()) AND requesting_user_role() = 'parent' ); END; $$; -- 10. Class policy referenced classId on Student; Student no longer has classId. Policy uses auth_user_classes() which we updated. -- Event/Announcement policies use auth_user_classes() - no change needed.