perf: optimize getEventTypeIdsFromTeamIdsFilter with raw SQL query (#27145)
* perf: optimize getEventTypeIdsFromTeamIdsFilter with raw SQL query Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: use prisma singleton for raw queries to fix runtime error Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: use context prisma for raw queries and update test mocks - Changed getEventTypeIdsFromTeamIdsFilter to use the context-passed prisma parameter instead of importing the singleton - Added $queryRaw mock to test file to support raw SQL queries - Updated test assertion to check for $queryRaw call instead of eventType.findMany The context prisma IS the same singleton at runtime (passed via tRPC context in createContext.ts). The test was failing because the mock didn't include $queryRaw. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Removed extra subquery where not needed * fix: restore subquery optimization for better query performance Restores the subquery structure that allows PostgreSQL to use the composite index on EventType(parentId, teamId) efficiently via Nested Loop joins, resulting in ~66x faster execution (2.46ms vs 164ms in production benchmarks). Identified by Cubic AI (confidence 9/10). Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * Fighting with AI --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin AI
parent
7efd9a43c9
commit
aaabcf4430
@@ -120,6 +120,7 @@ describe("getBookings - PBAC Permission Checks", () => {
|
||||
findUnique: vi.fn(),
|
||||
groupBy: vi.fn(),
|
||||
},
|
||||
$queryRaw: vi.fn().mockResolvedValue([]),
|
||||
} as unknown as PrismaClient;
|
||||
|
||||
// Create a comprehensive kysely mock that handles all chain methods
|
||||
@@ -338,7 +339,7 @@ describe("getBookings - PBAC Permission Checks", () => {
|
||||
it("should get event types from teams where user has booking.read permission", async () => {
|
||||
mockGetTeamIdsWithPermission.mockResolvedValue([1]);
|
||||
mockPrisma.user.findMany = vi.fn().mockResolvedValue([]);
|
||||
mockPrisma.eventType.findMany = vi.fn().mockResolvedValue([{ id: 10 }, { id: 11 }]);
|
||||
mockPrisma.$queryRaw = vi.fn().mockResolvedValue([{ id: 10 }, { id: 11 }]);
|
||||
mockPrisma.booking.groupBy = vi.fn().mockResolvedValue([]);
|
||||
|
||||
await getBookings({
|
||||
@@ -351,8 +352,8 @@ describe("getBookings - PBAC Permission Checks", () => {
|
||||
skip: 0,
|
||||
});
|
||||
|
||||
// Verify event types are fetched from team 1
|
||||
expect(mockPrisma.eventType.findMany).toHaveBeenCalled();
|
||||
// Verify event types are fetched from team 1 using raw SQL query
|
||||
expect(mockPrisma.$queryRaw).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import type { Kysely } from "kysely";
|
||||
import { type SelectQueryBuilder } from "kysely";
|
||||
import { jsonObjectFrom, jsonArrayFrom } from "kysely/helpers/postgres";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import getAllUserBookings from "@calcom/features/bookings/lib/getAllUserBookings";
|
||||
import { isTextFilterValue } from "@calcom/features/data-table/lib/utils";
|
||||
@@ -13,12 +9,13 @@ import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { Booking, Prisma, Prisma as PrismaClientType } from "@calcom/prisma/client";
|
||||
import { SchedulingType, BookingStatus, MembershipRole } from "@calcom/prisma/enums";
|
||||
import type { Booking, Prisma as PrismaClientType } from "@calcom/prisma/client";
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
import { BookingStatus, MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { Kysely, SelectQueryBuilder } from "kysely";
|
||||
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/postgres";
|
||||
import type { TrpcSessionUser } from "../../../types";
|
||||
import type { TGetInputSchema } from "./get.schema";
|
||||
|
||||
@@ -440,7 +437,8 @@ export async function getBookings({
|
||||
"Booking.metadata",
|
||||
"Booking.uid",
|
||||
eb
|
||||
.cast<Prisma.JsonValue>( // Target TypeScript type
|
||||
.cast<Prisma.JsonValue>(
|
||||
// Target TypeScript type
|
||||
eb.ref("Booking.responses"), // Source column
|
||||
"jsonb" // Target SQL type
|
||||
)
|
||||
@@ -784,7 +782,7 @@ type EnrichedUserData = {
|
||||
* @returns Bookings with attendees enriched with user data (name, email, avatarUrl, username)
|
||||
*/
|
||||
async function enrichAttendeesWithUserData<
|
||||
TBooking extends { attendees: ReadonlyArray<{ id: number; email: string }> }
|
||||
TBooking extends { attendees: ReadonlyArray<{ id: number; email: string }> },
|
||||
>(
|
||||
bookings: TBooking[],
|
||||
kysely: Kysely<DB>
|
||||
@@ -833,38 +831,40 @@ async function enrichAttendeesWithUserData<
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets event type IDs for the given team IDs using an optimized raw SQL query.
|
||||
*
|
||||
* This query uses a UNION to combine:
|
||||
* 1. Child event types whose parent belongs to the specified teams (managed event types)
|
||||
* 2. Direct team event types that belong to the specified teams
|
||||
*
|
||||
* The subquery structure `WHERE "parent"."id" IN (SELECT "id" FROM "EventType" WHERE "teamId" IN (...)))`
|
||||
* is intentional - it allows PostgreSQL to use the composite index on EventType(parentId, teamId)
|
||||
* efficiently via Nested Loop joins, resulting in ~66x faster execution compared to a direct
|
||||
* WHERE clause on parent.teamId (2.46ms vs 164ms in production benchmarks).
|
||||
*
|
||||
* @param prisma The Prisma client
|
||||
* @param teamIds Array of team IDs to filter by
|
||||
* @returns Array of event type IDs or undefined if no teamIds provided
|
||||
*/
|
||||
async function getEventTypeIdsFromTeamIdsFilter(prisma: PrismaClient, teamIds?: number[]) {
|
||||
if (!teamIds || teamIds.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [directTeamEventTypeIds, parentTeamEventTypeIds] = await Promise.all([
|
||||
prisma.eventType
|
||||
.findMany({
|
||||
where: {
|
||||
teamId: { in: teamIds },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
})
|
||||
.then((eventTypes) => eventTypes.map((eventType) => eventType.id)),
|
||||
const result = await prisma.$queryRaw<{ id: number }[]>`
|
||||
SELECT "child"."id"
|
||||
FROM "public"."EventType" AS "parent"
|
||||
LEFT JOIN "public"."EventType" "child" ON ("parent"."id") = ("child"."parentId")
|
||||
WHERE "parent"."id" IN (SELECT "id" FROM "public"."EventType" WHERE "teamId" IN (${Prisma.join(teamIds)}))
|
||||
AND "child"."id" IS NOT NULL
|
||||
UNION
|
||||
SELECT "parent"."id"
|
||||
FROM "public"."EventType" AS "parent"
|
||||
WHERE "parent"."teamId" IN (${Prisma.join(teamIds)})
|
||||
`;
|
||||
|
||||
prisma.eventType
|
||||
.findMany({
|
||||
where: {
|
||||
parent: {
|
||||
teamId: { in: teamIds },
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
})
|
||||
.then((eventTypes) => eventTypes.map((eventType) => eventType.id)),
|
||||
]);
|
||||
|
||||
return Array.from(new Set([...directTeamEventTypeIds, ...parentTeamEventTypeIds]));
|
||||
return result.map((row) => row.id);
|
||||
}
|
||||
|
||||
async function getAttendeeEmailsFromUserIdsFilter(
|
||||
|
||||
Reference in New Issue
Block a user