perf(db): add missing indexes, fix N+1 batch delete, add pagination limits

- Add indexes on Resource(blueprintId, roleId), DemandRequirement(roleId),
  Assignment(roleId) — commonly filtered FK columns that were missing indexes
- Replace N+1 batch delete pattern (2N queries) with findAllocationEntries()
  that does 2 total queries via findMany({ id: { in: ids } })
- Add take/skip pagination with default limit of 500 to listDemands and
  listAssignments to prevent unbounded result sets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-11 08:09:39 +02:00
parent 110e4ff1aa
commit c098cedf06
7 changed files with 264 additions and 158 deletions
+1
View File
@@ -50,6 +50,7 @@ export {
export {
findAllocationEntry,
findAllocationEntries,
loadAllocationEntry,
type AllocationEntryResolution,
} from "./use-cases/allocation/load-allocation-entry.js";
@@ -2,10 +2,7 @@ import type { Prisma, PrismaClient } from "@capakraken/db";
import type { AllocationWithDetails } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { buildSplitAllocationReadModel } from "./build-split-allocation-read-model.js";
import {
ASSIGNMENT_RELATIONS_INCLUDE,
type AssignmentWithRelations,
} from "./create-assignment.js";
import { ASSIGNMENT_RELATIONS_INCLUDE, type AssignmentWithRelations } from "./create-assignment.js";
import {
DEMAND_REQUIREMENT_RELATIONS_INCLUDE,
type DemandRequirementWithRelations,
@@ -40,9 +37,7 @@ function toDemandAllocationEntry(
}).allocations[0] as AllocationWithDetails;
}
function toAssignmentAllocationEntry(
assignment: AssignmentWithRelations,
): AllocationWithDetails {
function toAssignmentAllocationEntry(assignment: AssignmentWithRelations): AllocationWithDetails {
return buildSplitAllocationReadModel({
demandRequirements: [],
assignments: [assignment],
@@ -115,3 +110,50 @@ export async function loadAllocationEntry(
return resolved;
}
/**
* Batch-loads allocation entries by ID (2 queries total instead of 2N).
* Each ID is resolved against both demand_requirements and assignments.
* IDs that don't match either table are silently skipped.
*/
export async function findAllocationEntries(
db: DbClient,
ids: string[],
): Promise<AllocationEntryResolution[]> {
if (ids.length === 0) return [];
const [demandRequirements, assignments] = await Promise.all([
db.demandRequirement.findMany({
where: { id: { in: ids } },
include: DEMAND_REQUIREMENT_RELATIONS_INCLUDE,
}),
db.assignment.findMany({
where: { id: { in: ids } },
include: ASSIGNMENT_RELATIONS_INCLUDE,
}),
]);
const results: AllocationEntryResolution[] = [];
for (const dr of demandRequirements) {
results.push({
kind: "demand",
entry: toDemandAllocationEntry(dr),
demandRequirement: dr,
projectId: dr.projectId,
resourceId: null,
});
}
for (const a of assignments) {
results.push({
kind: "assignment",
entry: toAssignmentAllocationEntry(a),
assignment: a,
projectId: a.projectId,
resourceId: a.resourceId,
});
}
return results;
}