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:
Keith Williams
2026-01-22 13:26:49 -03:00
committed by GitHub
co-authored by Devin AI
parent 7efd9a43c9
commit aaabcf4430
2 changed files with 40 additions and 39 deletions
@@ -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(