Skip to content

Commit a20a546

Browse files
authored
perf(db): optimize recurring query paths (#7014)
* perf(db): optimize recurring query paths * perf(logs): batch keyset export reads * fix(workspaces): type nullable member count targets * fix(db): harden query performance changes * fix(logs): guard export stream cancellation
1 parent 9cecf0b commit a20a546

20 files changed

Lines changed: 21383 additions & 211 deletions

File tree

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { workflowExecutionLogs } from '@sim/db/schema'
5+
import {
6+
authMockFns,
7+
createMockRequest,
8+
dbChainMockFns,
9+
queueTableRows,
10+
resetDbChainMock,
11+
} from '@sim/testing'
12+
import { beforeEach, describe, expect, it, vi } from 'vitest'
13+
14+
const {
15+
mockCheckWorkspaceAccess,
16+
mockExpandFolderIdsWithDescendants,
17+
mockMapWithConcurrency,
18+
mockMaterializeExecutionDataForDisplay,
19+
} = vi.hoisted(() => ({
20+
mockCheckWorkspaceAccess: vi.fn(),
21+
mockExpandFolderIdsWithDescendants: vi.fn(),
22+
mockMapWithConcurrency: vi.fn(),
23+
mockMaterializeExecutionDataForDisplay: vi.fn(),
24+
}))
25+
26+
vi.mock('@/lib/workspaces/permissions/utils', () => ({
27+
checkWorkspaceAccess: mockCheckWorkspaceAccess,
28+
}))
29+
30+
vi.mock('@/lib/logs/folder-expansion', () => ({
31+
expandFolderIdsWithDescendants: mockExpandFolderIdsWithDescendants,
32+
}))
33+
34+
vi.mock('@/lib/logs/execution/trace-store', () => ({
35+
materializeExecutionDataForDisplay: mockMaterializeExecutionDataForDisplay,
36+
}))
37+
38+
vi.mock('@/lib/core/utils/concurrency', () => ({
39+
MATERIALIZE_CONCURRENCY: 20,
40+
mapWithConcurrency: mockMapWithConcurrency,
41+
}))
42+
43+
import { GET } from '@/app/api/logs/export/route'
44+
45+
const mockGetSession = authMockFns.mockGetSession
46+
const STARTED_AT = new Date('2026-08-23T12:00:00.000Z')
47+
48+
function makeRequest() {
49+
return createMockRequest(
50+
'GET',
51+
undefined,
52+
{},
53+
'http://localhost:3000/api/logs/export?workspaceId=workspace-1'
54+
)
55+
}
56+
57+
function logRow(index: number, overrides: Record<string, unknown> = {}) {
58+
const startedAt = new Date(STARTED_AT.getTime() - index * 1000)
59+
return {
60+
id: `log-${index.toString().padStart(4, '0')}`,
61+
workflowId: 'workflow-1',
62+
executionId: `execution-${index}`,
63+
level: 'info',
64+
trigger: 'manual',
65+
startedAt,
66+
startedAtCursor: startedAt.toISOString(),
67+
endedAt: new Date(STARTED_AT.getTime() - index * 1000 + 500),
68+
totalDurationMs: 500,
69+
costTotal: '0.01',
70+
executionData: { message: `message-${index}` },
71+
workflowName: 'Workflow',
72+
...overrides,
73+
}
74+
}
75+
76+
function flattenConditions(condition: unknown): Array<Record<string, unknown>> {
77+
if (!condition || typeof condition !== 'object') return []
78+
const node = condition as Record<string, unknown>
79+
if (Array.isArray(node.conditions)) {
80+
return node.conditions.flatMap(flattenConditions)
81+
}
82+
return [node]
83+
}
84+
85+
describe('GET /api/logs/export', () => {
86+
beforeEach(() => {
87+
vi.clearAllMocks()
88+
resetDbChainMock()
89+
mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
90+
mockCheckWorkspaceAccess.mockResolvedValue({ hasAccess: true })
91+
mockExpandFolderIdsWithDescendants.mockImplementation(
92+
async (_workspaceId: string, folderIds: string | undefined) => folderIds
93+
)
94+
mockMaterializeExecutionDataForDisplay.mockImplementation(
95+
async (executionData: Record<string, unknown> | null | undefined) => executionData ?? {}
96+
)
97+
mockMapWithConcurrency.mockImplementation(
98+
async (
99+
items: unknown[],
100+
_limit: number,
101+
mapper: (item: unknown, index: number) => Promise<unknown>
102+
) => Promise.all(items.map(mapper))
103+
)
104+
})
105+
106+
it('rejects unauthenticated exports before checking workspace access', async () => {
107+
mockGetSession.mockResolvedValueOnce(null)
108+
109+
const response = await GET(makeRequest())
110+
111+
expect(response.status).toBe(401)
112+
expect(mockCheckWorkspaceAccess).not.toHaveBeenCalled()
113+
expect(dbChainMockFns.where).not.toHaveBeenCalled()
114+
expect(mockMaterializeExecutionDataForDisplay).not.toHaveBeenCalled()
115+
})
116+
117+
it('returns only the CSV header when workspace access is denied', async () => {
118+
mockCheckWorkspaceAccess.mockResolvedValueOnce({ hasAccess: false })
119+
120+
const response = await GET(makeRequest())
121+
122+
expect(response.status).toBe(200)
123+
expect(await response.text()).toBe(
124+
'startedAt,level,workflow,trigger,durationMs,costTotal,workflowId,executionId,message,traceSpans\n'
125+
)
126+
expect(dbChainMockFns.where).not.toHaveBeenCalled()
127+
expect(mockMaterializeExecutionDataForDisplay).not.toHaveBeenCalled()
128+
})
129+
130+
it('materializes bounded chunks while preserving CSV row order', async () => {
131+
queueTableRows(
132+
workflowExecutionLogs,
133+
Array.from({ length: 45 }, (_, index) => logRow(index))
134+
)
135+
136+
const response = await GET(makeRequest())
137+
const lines = (await response.text()).trimEnd().split('\n')
138+
139+
expect(response.status).toBe(200)
140+
expect(mockMapWithConcurrency.mock.calls.map(([items]) => items.length)).toEqual([20, 20, 5])
141+
expect(lines).toHaveLength(46)
142+
expect(lines[1]).toContain('execution-0')
143+
expect(lines.at(-1)).toContain('execution-44')
144+
})
145+
146+
it('resumes full pages by startedAt and id without using OFFSET', async () => {
147+
const firstPage = Array.from({ length: 100 }, (_, index) => logRow(index))
148+
firstPage[99] = logRow(99, { startedAtCursor: '2026-08-23 11:58:21.000123' })
149+
const last = firstPage.at(-1)!
150+
const secondPage = [
151+
logRow(100, {
152+
id: 'log-0000-second',
153+
startedAt: last.startedAt,
154+
startedAtCursor: '2026-08-23 11:58:21.000122',
155+
}),
156+
]
157+
queueTableRows(workflowExecutionLogs, firstPage)
158+
queueTableRows(workflowExecutionLogs, secondPage)
159+
160+
const response = await GET(makeRequest())
161+
const lines = (await response.text()).trimEnd().split('\n')
162+
163+
expect(lines).toHaveLength(102)
164+
expect(dbChainMockFns.offset).not.toHaveBeenCalled()
165+
expect(dbChainMockFns.where).toHaveBeenCalledTimes(2)
166+
expect(dbChainMockFns.orderBy).toHaveBeenNthCalledWith(
167+
1,
168+
expect.objectContaining({
169+
type: 'desc',
170+
column: workflowExecutionLogs.startedAt,
171+
}),
172+
expect.objectContaining({
173+
type: 'desc',
174+
column: workflowExecutionLogs.id,
175+
})
176+
)
177+
178+
const cursorConditions = flattenConditions(dbChainMockFns.where.mock.calls[1][0])
179+
const timestampConditions = cursorConditions.filter(
180+
(condition) => condition.left === workflowExecutionLogs.startedAt
181+
)
182+
expect(timestampConditions.map((condition) => condition.type)).toEqual(['lt', 'eq'])
183+
for (const condition of timestampConditions) {
184+
expect(condition.right).not.toBeInstanceOf(Date)
185+
expect(condition.right).toEqual(
186+
expect.objectContaining({ values: expect.arrayContaining([last.startedAtCursor]) })
187+
)
188+
}
189+
expect(cursorConditions).toContainEqual(
190+
expect.objectContaining({
191+
type: 'lt',
192+
left: workflowExecutionLogs.id,
193+
right: last.id,
194+
})
195+
)
196+
})
197+
198+
it('does not load the next database page until the current row is consumed', async () => {
199+
queueTableRows(
200+
workflowExecutionLogs,
201+
Array.from({ length: 100 }, (_, index) => logRow(index))
202+
)
203+
queueTableRows(workflowExecutionLogs, [logRow(1)])
204+
205+
const response = await GET(makeRequest())
206+
const reader = response.body!.getReader()
207+
208+
await reader.read()
209+
expect(dbChainMockFns.where).not.toHaveBeenCalled()
210+
211+
await reader.read()
212+
expect(dbChainMockFns.where).toHaveBeenCalledTimes(1)
213+
214+
await reader.cancel()
215+
expect(dbChainMockFns.where).toHaveBeenCalledTimes(1)
216+
})
217+
218+
it('stops a pending pull cleanly when the reader cancels', async () => {
219+
queueTableRows(workflowExecutionLogs, [logRow(0)])
220+
let resolveMaterialization: ((value: unknown[]) => void) | undefined
221+
mockMapWithConcurrency.mockImplementationOnce(
222+
() =>
223+
new Promise((resolve) => {
224+
resolveMaterialization = resolve
225+
})
226+
)
227+
228+
const response = await GET(makeRequest())
229+
const reader = response.body!.getReader()
230+
await reader.read()
231+
232+
const pendingRead = reader.read()
233+
await vi.waitFor(() => expect(mockMapWithConcurrency).toHaveBeenCalledTimes(1))
234+
const cancellation = reader.cancel()
235+
resolveMaterialization?.([{ message: 'message-0' }])
236+
237+
await expect(Promise.all([pendingRead, cancellation])).resolves.toBeDefined()
238+
})
239+
})

0 commit comments

Comments
 (0)