diff --git a/AGENTS.md b/AGENTS.md index ebfbecf5df..619547aa62 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -119,3 +119,4 @@ To see all development ports, refer to the index.html of `apps/dev-launchpad/pub ### Code-related - Use ES6 maps instead of records wherever you can. +- **Read replicas for raw Prisma queries**: When writing raw SQL queries (`$queryRaw`, `$queryRawUnsafe`), always use `$replica()` for read-only queries (e.g. `globalPrismaClient.$replica().$queryRaw\`SELECT ...\``). This routes reads to the database replica and reduces load on the primary. Do NOT use `$replica()` for queries inside transactions or queries containing writes (INSERT/UPDATE/DELETE, even in CTEs). diff --git a/apps/backend/src/app/api/latest/auth/cli/complete/route.tsx b/apps/backend/src/app/api/latest/auth/cli/complete/route.tsx index 908ea7ea00..1187c38427 100644 --- a/apps/backend/src/app/api/latest/auth/cli/complete/route.tsx +++ b/apps/backend/src/app/api/latest/auth/cli/complete/route.tsx @@ -103,7 +103,7 @@ async function getPendingCliAuthAttempt(tenancy: Tenancy, loginCode: string) { // CliAuthAttempt lives in the tenancy's source-of-truth DB, consistent with cli/poll/route.tsx. const prisma = await getPrismaClientForTenancy(tenancy); const schema = await getPrismaSchemaForTenancy(tenancy); - const rows = await prisma.$queryRaw(Prisma.sql` + const rows = await prisma.$replica().$queryRaw(Prisma.sql` SELECT "id", "tenancyId", @@ -130,7 +130,7 @@ async function getPendingCliAuthAttempt(tenancy: Tenancy, loginCode: string) { async function getRefreshTokenSession(tenancyId: string, refreshToken: string) { // ProjectUserRefreshToken lives in the global DB (see tokens.tsx and oauth/model.tsx). - const rows = await globalPrismaClient.$queryRaw(Prisma.sql` + const rows = await globalPrismaClient.$replica().$queryRaw(Prisma.sql` SELECT "id", "tenancyId", diff --git a/apps/backend/src/app/api/latest/auth/cli/poll/route.tsx b/apps/backend/src/app/api/latest/auth/cli/poll/route.tsx index 387c196cf1..44c17681fb 100644 --- a/apps/backend/src/app/api/latest/auth/cli/poll/route.tsx +++ b/apps/backend/src/app/api/latest/auth/cli/poll/route.tsx @@ -48,7 +48,7 @@ export const POST = createSmartRouteHandler({ const prisma = await getPrismaClientForTenancy(tenancy); const schema = await getPrismaSchemaForTenancy(tenancy); - const cliAuthRows = await prisma.$queryRaw(Prisma.sql` + const cliAuthRows = await prisma.$replica().$queryRaw(Prisma.sql` SELECT "id", "refreshToken", diff --git a/apps/backend/src/app/api/latest/auth/cli/route.tsx b/apps/backend/src/app/api/latest/auth/cli/route.tsx index 5d122c11d7..f6ac433b4b 100644 --- a/apps/backend/src/app/api/latest/auth/cli/route.tsx +++ b/apps/backend/src/app/api/latest/auth/cli/route.tsx @@ -42,7 +42,7 @@ export const POST = createSmartRouteHandler({ let anonRefreshToken: string | null = null; if (anon_refresh_token != null) { - const refreshTokenRows = await globalPrismaClient.$queryRaw(Prisma.sql` + const refreshTokenRows = await globalPrismaClient.$replica().$queryRaw(Prisma.sql` SELECT "tenancyId", "projectUserId", "expiresAt" FROM "ProjectUserRefreshToken" WHERE "refreshToken" = ${anon_refresh_token} diff --git a/apps/backend/src/app/api/latest/internal/external-db-sync/status/route.ts b/apps/backend/src/app/api/latest/internal/external-db-sync/status/route.ts index 88845389e6..7ea19fb66b 100644 --- a/apps/backend/src/app/api/latest/internal/external-db-sync/status/route.ts +++ b/apps/backend/src/app/api/latest/internal/external-db-sync/status/route.ts @@ -310,7 +310,7 @@ async function fetchInternalStats(tenancyId: string | null) { ? Prisma.sql`WHERE "tenancyId" = ${tenancyId}::uuid` : Prisma.sql``; - const projectUserStatsRow = (await globalPrismaClient.$queryRaw` + const projectUserStatsRow = (await globalPrismaClient.$replica().$queryRaw` SELECT COUNT(*)::bigint AS "total", COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending", @@ -321,7 +321,7 @@ async function fetchInternalStats(tenancyId: string | null) { ${tenancyWhere} `).at(0) ?? throwErr("Project user stats query returned no rows."); - const contactChannelStatsRow = (await globalPrismaClient.$queryRaw` + const contactChannelStatsRow = (await globalPrismaClient.$replica().$queryRaw` SELECT COUNT(*)::bigint AS "total", COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending", @@ -332,7 +332,7 @@ async function fetchInternalStats(tenancyId: string | null) { ${tenancyWhere} `).at(0) ?? throwErr("Contact channel stats query returned no rows."); - const teamStatsRow = (await globalPrismaClient.$queryRaw` + const teamStatsRow = (await globalPrismaClient.$replica().$queryRaw` SELECT COUNT(*)::bigint AS "total", COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending", @@ -343,7 +343,7 @@ async function fetchInternalStats(tenancyId: string | null) { ${tenancyWhere} `).at(0) ?? throwErr("Team stats query returned no rows."); - const teamMemberStatsRow = (await globalPrismaClient.$queryRaw` + const teamMemberStatsRow = (await globalPrismaClient.$replica().$queryRaw` SELECT COUNT(*)::bigint AS "total", COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending", @@ -354,7 +354,7 @@ async function fetchInternalStats(tenancyId: string | null) { ${tenancyWhere} `).at(0) ?? throwErr("Team member stats query returned no rows."); - const teamPermissionStatsRow = (await globalPrismaClient.$queryRaw` + const teamPermissionStatsRow = (await globalPrismaClient.$replica().$queryRaw` SELECT COUNT(*)::bigint AS "total", COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending", @@ -365,7 +365,7 @@ async function fetchInternalStats(tenancyId: string | null) { ${tenancyWhere} `).at(0) ?? throwErr("Team permission stats query returned no rows."); - const teamInvitationStatsRow = (await globalPrismaClient.$queryRaw` + const teamInvitationStatsRow = (await globalPrismaClient.$replica().$queryRaw` SELECT COUNT(*)::bigint AS "total", COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending", @@ -378,7 +378,7 @@ async function fetchInternalStats(tenancyId: string | null) { : Prisma.sql`WHERE "type" = 'TEAM_INVITATION'`} `).at(0) ?? throwErr("Team invitation stats query returned no rows."); - const emailOutboxStatsRow = (await globalPrismaClient.$queryRaw` + const emailOutboxStatsRow = (await globalPrismaClient.$replica().$queryRaw` SELECT COUNT(*)::bigint AS "total", COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending", @@ -389,7 +389,7 @@ async function fetchInternalStats(tenancyId: string | null) { ${tenancyWhere} `).at(0) ?? throwErr("Email outbox stats query returned no rows."); - const projectPermissionStatsRow = (await globalPrismaClient.$queryRaw` + const projectPermissionStatsRow = (await globalPrismaClient.$replica().$queryRaw` SELECT COUNT(*)::bigint AS "total", COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending", @@ -400,7 +400,7 @@ async function fetchInternalStats(tenancyId: string | null) { ${tenancyWhere} `).at(0) ?? throwErr("Project permission stats query returned no rows."); - const notificationPreferenceStatsRow = (await globalPrismaClient.$queryRaw` + const notificationPreferenceStatsRow = (await globalPrismaClient.$replica().$queryRaw` SELECT COUNT(*)::bigint AS "total", COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending", @@ -411,7 +411,7 @@ async function fetchInternalStats(tenancyId: string | null) { ${tenancyWhere} `).at(0) ?? throwErr("Notification preference stats query returned no rows."); - const refreshTokenStatsRow = (await globalPrismaClient.$queryRaw` + const refreshTokenStatsRow = (await globalPrismaClient.$replica().$queryRaw` SELECT COUNT(*)::bigint AS "total", COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending", @@ -422,7 +422,7 @@ async function fetchInternalStats(tenancyId: string | null) { ${tenancyWhere} `).at(0) ?? throwErr("Refresh token stats query returned no rows."); - const connectedAccountStatsRow = (await globalPrismaClient.$queryRaw` + const connectedAccountStatsRow = (await globalPrismaClient.$replica().$queryRaw` SELECT COUNT(*)::bigint AS "total", COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending", @@ -433,7 +433,7 @@ async function fetchInternalStats(tenancyId: string | null) { ${tenancyWhere} `).at(0) ?? throwErr("Connected account stats query returned no rows."); - const deletedRowStatsRow = (await globalPrismaClient.$queryRaw` + const deletedRowStatsRow = (await globalPrismaClient.$replica().$queryRaw` SELECT COUNT(*)::bigint AS "total", COUNT(*) FILTER (WHERE "shouldUpdateSequenceId" = TRUE OR "sequenceId" IS NULL)::bigint AS "pending", @@ -444,7 +444,7 @@ async function fetchInternalStats(tenancyId: string | null) { ${tenancyWhere} `).at(0) ?? throwErr("Deleted row stats query returned no rows."); - const deletedRowsByTableRows = await globalPrismaClient.$queryRaw` + const deletedRowsByTableRows = await globalPrismaClient.$replica().$queryRaw` SELECT "tableName" AS "table_name", COUNT(*)::bigint AS "total", @@ -462,7 +462,7 @@ async function fetchInternalStats(tenancyId: string | null) { ? Prisma.sql`AND ("qstashOptions"->'body'->>'tenancyId') = ${tenancyId}` : Prisma.sql``; - const outgoingStatsRow = (await globalPrismaClient.$queryRaw` + const outgoingStatsRow = (await globalPrismaClient.$replica().$queryRaw` SELECT COUNT(*)::bigint AS "total", COUNT(*) FILTER (WHERE "startedFulfillingAt" IS NULL)::bigint AS "pending", @@ -1109,13 +1109,13 @@ export const GET = createSmartRouteHandler({ const globalStats = shouldIncludeGlobal ? currentStats : null; const globalTenanciesCount = shouldIncludeGlobal - ? (await globalPrismaClient.$queryRaw` + ? (await globalPrismaClient.$replica().$queryRaw` SELECT COUNT(*)::bigint AS "total" FROM "Tenancy" `).at(0) ?? throwErr("Tenancy count query returned no rows.") : null; const globalDbSyncCount = shouldIncludeGlobal - ? (await globalPrismaClient.$queryRaw` + ? (await globalPrismaClient.$replica().$queryRaw` SELECT COUNT(*)::bigint AS "total" FROM "EnvironmentConfigOverride" WHERE ("config"->'dbSync'->'externalDatabases') IS NOT NULL diff --git a/apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx b/apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx index 7c85618c0b..a961c54ade 100644 --- a/apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx +++ b/apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx @@ -83,7 +83,7 @@ async function assertLocalEmulatorOwnerTeamReadiness() { } async function getOrCreateLocalEmulatorProjectId(absoluteFilePath: string): Promise<{ projectId: string, created: boolean }> { - const existingRows = await globalPrismaClient.$queryRaw(Prisma.sql` + const existingRows = await globalPrismaClient.$replica().$queryRaw(Prisma.sql` SELECT "projectId" FROM "LocalEmulatorProject" WHERE "absoluteFilePath" = ${absoluteFilePath} @@ -187,7 +187,7 @@ async function getOrCreateCredentials(projectId: string) { } async function syncLocalEmulatorOnboardingStatus(projectId: string, showOnboarding: boolean): Promise { - const onboardingStateColumnExistsRows = await globalPrismaClient.$queryRaw>(Prisma.sql` + const onboardingStateColumnExistsRows = await globalPrismaClient.$replica().$queryRaw>(Prisma.sql` SELECT EXISTS ( SELECT 1 FROM information_schema.columns @@ -198,7 +198,7 @@ async function syncLocalEmulatorOnboardingStatus(projectId: string, showOnboardi `); const onboardingStateColumnExists = onboardingStateColumnExistsRows[0]?.exists === true; - const rows = await globalPrismaClient.$queryRaw>(Prisma.sql` + const rows = await globalPrismaClient.$replica().$queryRaw>(Prisma.sql` SELECT "onboardingStatus" FROM "Project" WHERE "id" = ${projectId} @@ -385,7 +385,7 @@ export const GET = createSmartRouteHandler({ throw new StatusError(StatusError.BadRequest, LOCAL_EMULATOR_ONLY_ENDPOINT_MESSAGE); } - const rows = await globalPrismaClient.$queryRaw(Prisma.sql` + const rows = await globalPrismaClient.$replica().$queryRaw(Prisma.sql` SELECT "projectId", "absoluteFilePath", "updatedAt" FROM "LocalEmulatorProject" ORDER BY "updatedAt" DESC diff --git a/apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx b/apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx index 06a8d561a7..4806cbcb27 100644 --- a/apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx +++ b/apps/backend/src/app/api/latest/internal/payments/transactions/route.tsx @@ -651,7 +651,7 @@ async function getTransactions(options: { LIMIT ${options.limit + 1} `; - const rawRows = await options.prisma.$queryRaw>`${Prisma.raw(sql)}`; + const rawRows = await options.prisma.$replica().$queryRaw>`${Prisma.raw(sql)}`; const parsedRows = rawRows.map((row) => { const parsed = readLedgerTransactionRow(row.rowData); return { @@ -711,7 +711,7 @@ async function getTransactions(options: { FROM (${baseSql}) AS "__rows" WHERE ${refundWhereClauses.join("\n AND ")} `; - refundRows = await options.prisma.$queryRaw>`${Prisma.raw(refundSql)}`; + refundRows = await options.prisma.$replica().$queryRaw>`${Prisma.raw(refundSql)}`; } const resolvedAdjustedByLookup = buildAdjustedByLookupFromRefundRows(refundRows.map((row) => row.rowData)); diff --git a/apps/backend/src/app/api/latest/internal/session-replays/session-replay-admin-rows.ts b/apps/backend/src/app/api/latest/internal/session-replays/session-replay-admin-rows.ts index fef7cc6823..ce977320e7 100644 --- a/apps/backend/src/app/api/latest/internal/session-replays/session-replay-admin-rows.ts +++ b/apps/backend/src/app/api/latest/internal/session-replays/session-replay-admin-rows.ts @@ -24,7 +24,7 @@ export async function querySessionReplayAdminRows(options: { suffixSql: Prisma.Sql, }): Promise { const { prisma, schema, tenancyId, suffixSql } = options; - return await prisma.$queryRaw` + return await prisma.$replica().$queryRaw` SELECT sr."id", sr."projectUserId", diff --git a/apps/backend/src/lib/conversations.tsx b/apps/backend/src/lib/conversations.tsx index 04f22db8e5..9b11d660bc 100644 --- a/apps/backend/src/lib/conversations.tsx +++ b/apps/backend/src/lib/conversations.tsx @@ -247,7 +247,7 @@ async function getConversationRow(options: { conversationId: string, viewerProjectUserId?: string, }) { - const rows = await globalPrismaClient.$queryRaw(Prisma.sql` + const rows = await globalPrismaClient.$replica().$queryRaw(Prisma.sql` SELECT c.id AS "conversationId", c."projectUserId" AS "userId", @@ -297,7 +297,7 @@ async function getConversationState(options: { conversationId: string, viewerProjectUserId?: string, }) { - const rows = await globalPrismaClient.$queryRaw(Prisma.sql` + const rows = await globalPrismaClient.$replica().$queryRaw(Prisma.sql` SELECT c.id AS "conversationId", c."projectUserId" AS "userId", @@ -417,7 +417,7 @@ export async function listConversationSummaries(options: { const limit = options.limit ?? 200; const offset = options.offset ?? 0; - const rows = await globalPrismaClient.$queryRaw(Prisma.sql` + const rows = await globalPrismaClient.$replica().$queryRaw(Prisma.sql` SELECT c.id AS "conversationId", c."projectUserId" AS "userId", @@ -502,7 +502,7 @@ export async function getConversationDetail(options: { throw new StatusError(404, "Conversation not found."); } - const messageRows = await globalPrismaClient.$queryRaw(Prisma.sql` + const messageRows = await globalPrismaClient.$replica().$queryRaw(Prisma.sql` SELECT cm.id, cm."messageType", @@ -528,7 +528,7 @@ export async function getConversationDetail(options: { const messages = messageRows.map((row) => messageFromRow(row, conversation)); const latestMessage = messages.at(-1) ?? throwErr("Conversations must contain at least one message"); - const entryPointRows = await globalPrismaClient.$queryRaw(Prisma.sql` + const entryPointRows = await globalPrismaClient.$replica().$queryRaw(Prisma.sql` SELECT cep.id, cep."channelType", diff --git a/apps/backend/src/lib/development-environment.ts b/apps/backend/src/lib/development-environment.ts index 3b0c3a964a..0a0ff51d4c 100644 --- a/apps/backend/src/lib/development-environment.ts +++ b/apps/backend/src/lib/development-environment.ts @@ -8,7 +8,7 @@ export const DEVELOPMENT_ENVIRONMENT_ENV_CONFIG_BLOCKED_MESSAGE = export type ConfigOverrideWriteLevel = "project" | "branch" | "environment"; export async function isDevelopmentEnvironmentProject(projectId: string): Promise { - const rows = await globalPrismaClient.$queryRaw>(Prisma.sql` + const rows = await globalPrismaClient.$replica().$queryRaw>(Prisma.sql` SELECT "isDevelopmentEnvironment" FROM "Project" WHERE "id" = ${projectId} diff --git a/apps/backend/src/lib/managed-email-domains.tsx b/apps/backend/src/lib/managed-email-domains.tsx index c31c3960c6..9b4c609a47 100644 --- a/apps/backend/src/lib/managed-email-domains.tsx +++ b/apps/backend/src/lib/managed-email-domains.tsx @@ -103,7 +103,7 @@ export async function getManagedEmailDomainByTenancyAndSubdomain(options: { tenancyId: string, subdomain: string, }): Promise { - const rows = await globalPrismaClient.$queryRaw(Prisma.sql` + const rows = await globalPrismaClient.$replica().$queryRaw(Prisma.sql` SELECT * FROM "ManagedEmailDomain" WHERE "tenancyId" = ${options.tenancyId} @@ -117,7 +117,7 @@ export async function getManagedEmailDomainByTenancyAndSubdomain(options: { } export async function getManagedEmailDomainByResendDomainId(resendDomainId: string): Promise { - const rows = await globalPrismaClient.$queryRaw(Prisma.sql` + const rows = await globalPrismaClient.$replica().$queryRaw(Prisma.sql` SELECT * FROM "ManagedEmailDomain" WHERE "resendDomainId" = ${resendDomainId} @@ -216,7 +216,7 @@ export async function markManagedEmailDomainApplied(id: string): Promise { - const rows = await globalPrismaClient.$queryRaw(Prisma.sql` + const rows = await globalPrismaClient.$replica().$queryRaw(Prisma.sql` SELECT * FROM "ManagedEmailDomain" WHERE "tenancyId" = ${tenancyId}