Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@
## 2025-02-23 - [Parallelize Independent Queries with Promise.all()]
**Learning:** Sequential `for...of` loops that `await` independent database queries (like iterating over active school years to fetch report card discovery units) create severe N+1 bottlenecks.
**Action:** Always replace sequential `await` loops for read-only queries with `Promise.all()` mapped over the array to allow the database driver to process the independent queries concurrently, effectively eliminating the N+1 I/O wait.

## 2024-05-18 - [Bulk Insert for Check-Then-Act Operations]
**Learning:** Found sequential `await` inside a `for...of` loop in `createBulkGrades` inside `apps/school/src/school/functions/student-grades.ts` for validating class existence and inserting grades individually. While `Promise.all` is unsafe for check-then-act upserts or inserts due to race conditions, sequential loops create severe N+1 bottlenecks.
**Action:** When inserting multiple records, validate the condition (e.g. class ownership) once, and then use Drizzle's `db.insert(table).values(array).returning()` to execute the bulk insertion in a single I/O roundtrip.
47 changes: 25 additions & 22 deletions apps/school/src/school/functions/student-grades.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,30 +101,33 @@ export const createBulkGrades = authServerFn
await requirePermission('grades', 'create')

const gradeDate = data.gradeDate ?? new Date().toISOString().split('T')[0]
const results = []

for (const gradeItem of data.grades) {
const result = await gradeQueries.createStudentGrade(schoolId, {
id: crypto.randomUUID(),
studentId: gradeItem.studentId,
classId: data.classId,
subjectId: data.subjectId,
termId: data.termId,
teacherId: data.teacherId,
value: String(gradeItem.value),
type: data.type,
weight: data.weight ?? 1,
description: data.description,
gradeDate,
status: 'draft',
})

if (R.isFailure(result))
return { success: false as const, error: 'BULK_GRADE_CREATE_FAILED' }

results.push(result.value)

if (data.grades.length === 0) {
return { success: true as const, data: { count: 0, grades: [] } }
}

const gradesToInsert = data.grades.map(gradeItem => ({
id: crypto.randomUUID(),
studentId: gradeItem.studentId,
classId: data.classId,
subjectId: data.subjectId,
termId: data.termId,
teacherId: data.teacherId,
value: String(gradeItem.value),
type: data.type,
weight: data.weight ?? 1,
description: data.description,
gradeDate,
status: 'draft' as const,
}))

const result = await gradeQueries.createBulkStudentGrades(schoolId, gradesToInsert)

if (R.isFailure(result))
return { success: false as const, error: 'BULK_GRADE_CREATE_FAILED' }

const results = result.value

await createAuditLog({
schoolId,
userId,
Expand Down
26 changes: 26 additions & 0 deletions packages/data-ops/src/queries/grades.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,32 @@ export async function createStudentGrade(schoolId: string, data: StudentGradeIns
)
}

export async function createBulkStudentGrades(schoolId: string, data: StudentGradeInsert[]): R.ResultAsync<typeof studentGrades.$inferSelect[], DatabaseError> {
const db = getDb()
if (data.length === 0)
return R.ok([])

return R.pipe(
R.try({
try: async () => {
// Validation: Class must belong to school (assuming all grades in bulk are for the same class as per UI usage)
const classId = data[0]!.classId
const classExists = await db.query.classes.findFirst({
where: and(eq(classes.id, classId), eq(classes.schoolId, schoolId)),
columns: { id: true },
})
if (!classExists) {
throw new DatabaseError('PERMISSION_DENIED', getNestedErrorMessage('auth', 'noSchoolContext'))
}

return await db.insert(studentGrades).values(data).returning()
},
catch: e => DatabaseError.from(e),
}),
R.mapError(tapLogErr(databaseLogger, { schoolId, dataCount: data.length, action: 'Creating bulk student grades' })),
)
}

export async function updateStudentGrade(schoolId: string, id: string, data: Partial<StudentGradeInsert>): R.ResultAsync<typeof studentGrades.$inferSelect, DatabaseError> {
const db = getDb()
return R.pipe(
Expand Down
Loading