Route read-only raw Prisma queries to read replica (#1467)

This commit is contained in:
Konsti Wohlwend 2026-05-22 11:16:11 -07:00 committed by GitHub
parent 0c6e135c30
commit 197ad09eea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 37 additions and 36 deletions

View File

@ -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).

View File

@ -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<CliAuthAttemptRow[]>(Prisma.sql`
const rows = await prisma.$replica().$queryRaw<CliAuthAttemptRow[]>(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<RefreshTokenRow[]>(Prisma.sql`
const rows = await globalPrismaClient.$replica().$queryRaw<RefreshTokenRow[]>(Prisma.sql`
SELECT
"id",
"tenancyId",

View File

@ -48,7 +48,7 @@ export const POST = createSmartRouteHandler({
const prisma = await getPrismaClientForTenancy(tenancy);
const schema = await getPrismaSchemaForTenancy(tenancy);
const cliAuthRows = await prisma.$queryRaw<CliAuthAttemptRow[]>(Prisma.sql`
const cliAuthRows = await prisma.$replica().$queryRaw<CliAuthAttemptRow[]>(Prisma.sql`
SELECT
"id",
"refreshToken",

View File

@ -42,7 +42,7 @@ export const POST = createSmartRouteHandler({
let anonRefreshToken: string | null = null;
if (anon_refresh_token != null) {
const refreshTokenRows = await globalPrismaClient.$queryRaw<RefreshTokenRow[]>(Prisma.sql`
const refreshTokenRows = await globalPrismaClient.$replica().$queryRaw<RefreshTokenRow[]>(Prisma.sql`
SELECT "tenancyId", "projectUserId", "expiresAt"
FROM "ProjectUserRefreshToken"
WHERE "refreshToken" = ${anon_refresh_token}

View File

@ -310,7 +310,7 @@ async function fetchInternalStats(tenancyId: string | null) {
? Prisma.sql`WHERE "tenancyId" = ${tenancyId}::uuid`
: Prisma.sql``;
const projectUserStatsRow = (await globalPrismaClient.$queryRaw<SequenceStatsRow[]>`
const projectUserStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
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<SequenceStatsRow[]>`
const contactChannelStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
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<SequenceStatsRow[]>`
const teamStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
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<SequenceStatsRow[]>`
const teamMemberStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
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<SequenceStatsRow[]>`
const teamPermissionStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
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<SequenceStatsRow[]>`
const teamInvitationStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
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<SequenceStatsRow[]>`
const emailOutboxStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
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<SequenceStatsRow[]>`
const projectPermissionStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
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<SequenceStatsRow[]>`
const notificationPreferenceStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
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<SequenceStatsRow[]>`
const refreshTokenStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
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<SequenceStatsRow[]>`
const connectedAccountStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
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<SequenceStatsRow[]>`
const deletedRowStatsRow = (await globalPrismaClient.$replica().$queryRaw<SequenceStatsRow[]>`
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<DeletedRowStatsRow[]>`
const deletedRowsByTableRows = await globalPrismaClient.$replica().$queryRaw<DeletedRowStatsRow[]>`
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<OutgoingStatsRow[]>`
const outgoingStatsRow = (await globalPrismaClient.$replica().$queryRaw<OutgoingStatsRow[]>`
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<CountRow[]>`
? (await globalPrismaClient.$replica().$queryRaw<CountRow[]>`
SELECT COUNT(*)::bigint AS "total"
FROM "Tenancy"
`).at(0) ?? throwErr("Tenancy count query returned no rows.")
: null;
const globalDbSyncCount = shouldIncludeGlobal
? (await globalPrismaClient.$queryRaw<CountRow[]>`
? (await globalPrismaClient.$replica().$queryRaw<CountRow[]>`
SELECT COUNT(*)::bigint AS "total"
FROM "EnvironmentConfigOverride"
WHERE ("config"->'dbSync'->'externalDatabases') IS NOT NULL

View File

@ -83,7 +83,7 @@ async function assertLocalEmulatorOwnerTeamReadiness() {
}
async function getOrCreateLocalEmulatorProjectId(absoluteFilePath: string): Promise<{ projectId: string, created: boolean }> {
const existingRows = await globalPrismaClient.$queryRaw<LocalEmulatorProjectMappingRow[]>(Prisma.sql`
const existingRows = await globalPrismaClient.$replica().$queryRaw<LocalEmulatorProjectMappingRow[]>(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<ProjectOnboardingStatus> {
const onboardingStateColumnExistsRows = await globalPrismaClient.$queryRaw<Array<{ exists: boolean }>>(Prisma.sql`
const onboardingStateColumnExistsRows = await globalPrismaClient.$replica().$queryRaw<Array<{ exists: boolean }>>(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<Array<{ onboardingStatus: string }>>(Prisma.sql`
const rows = await globalPrismaClient.$replica().$queryRaw<Array<{ onboardingStatus: string }>>(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<LocalEmulatorProjectListRow[]>(Prisma.sql`
const rows = await globalPrismaClient.$replica().$queryRaw<LocalEmulatorProjectListRow[]>(Prisma.sql`
SELECT "projectId", "absoluteFilePath", "updatedAt"
FROM "LocalEmulatorProject"
ORDER BY "updatedAt" DESC

View File

@ -651,7 +651,7 @@ async function getTransactions(options: {
LIMIT ${options.limit + 1}
`;
const rawRows = await options.prisma.$queryRaw<Array<{ rowData: unknown }>>`${Prisma.raw(sql)}`;
const rawRows = await options.prisma.$replica().$queryRaw<Array<{ rowData: unknown }>>`${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<Array<{ rowData: unknown }>>`${Prisma.raw(refundSql)}`;
refundRows = await options.prisma.$replica().$queryRaw<Array<{ rowData: unknown }>>`${Prisma.raw(refundSql)}`;
}
const resolvedAdjustedByLookup = buildAdjustedByLookupFromRefundRows(refundRows.map((row) => row.rowData));

View File

@ -24,7 +24,7 @@ export async function querySessionReplayAdminRows(options: {
suffixSql: Prisma.Sql,
}): Promise<SessionReplayAdminListRow[]> {
const { prisma, schema, tenancyId, suffixSql } = options;
return await prisma.$queryRaw<SessionReplayAdminListRow[]>`
return await prisma.$replica().$queryRaw<SessionReplayAdminListRow[]>`
SELECT
sr."id",
sr."projectUserId",

View File

@ -247,7 +247,7 @@ async function getConversationRow(options: {
conversationId: string,
viewerProjectUserId?: string,
}) {
const rows = await globalPrismaClient.$queryRaw<DbConversationRow[]>(Prisma.sql`
const rows = await globalPrismaClient.$replica().$queryRaw<DbConversationRow[]>(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<ConversationStateRow[]>(Prisma.sql`
const rows = await globalPrismaClient.$replica().$queryRaw<ConversationStateRow[]>(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<ConversationSummaryRow[]>(Prisma.sql`
const rows = await globalPrismaClient.$replica().$queryRaw<ConversationSummaryRow[]>(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<ConversationMessageRow[]>(Prisma.sql`
const messageRows = await globalPrismaClient.$replica().$queryRaw<ConversationMessageRow[]>(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<ConversationEntryPointRow[]>(Prisma.sql`
const entryPointRows = await globalPrismaClient.$replica().$queryRaw<ConversationEntryPointRow[]>(Prisma.sql`
SELECT
cep.id,
cep."channelType",

View File

@ -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<boolean> {
const rows = await globalPrismaClient.$queryRaw<Array<{ isDevelopmentEnvironment: boolean }>>(Prisma.sql`
const rows = await globalPrismaClient.$replica().$queryRaw<Array<{ isDevelopmentEnvironment: boolean }>>(Prisma.sql`
SELECT "isDevelopmentEnvironment"
FROM "Project"
WHERE "id" = ${projectId}

View File

@ -103,7 +103,7 @@ export async function getManagedEmailDomainByTenancyAndSubdomain(options: {
tenancyId: string,
subdomain: string,
}): Promise<ManagedEmailDomain | null> {
const rows = await globalPrismaClient.$queryRaw<ManagedEmailDomainRow[]>(Prisma.sql`
const rows = await globalPrismaClient.$replica().$queryRaw<ManagedEmailDomainRow[]>(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<ManagedEmailDomain | null> {
const rows = await globalPrismaClient.$queryRaw<ManagedEmailDomainRow[]>(Prisma.sql`
const rows = await globalPrismaClient.$replica().$queryRaw<ManagedEmailDomainRow[]>(Prisma.sql`
SELECT *
FROM "ManagedEmailDomain"
WHERE "resendDomainId" = ${resendDomainId}
@ -216,7 +216,7 @@ export async function markManagedEmailDomainApplied(id: string): Promise<Managed
}
export async function listManagedEmailDomainsForTenancy(tenancyId: string): Promise<ManagedEmailDomain[]> {
const rows = await globalPrismaClient.$queryRaw<ManagedEmailDomainRow[]>(Prisma.sql`
const rows = await globalPrismaClient.$replica().$queryRaw<ManagedEmailDomainRow[]>(Prisma.sql`
SELECT *
FROM "ManagedEmailDomain"
WHERE "tenancyId" = ${tenancyId}