Skip to content

Commit 3ca60d2

Browse files
committed
fix(db): harden query performance changes
1 parent ff7c610 commit 3ca60d2

7 files changed

Lines changed: 105 additions & 187 deletions

File tree

apps/sim/app/api/logs/export/route.test.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,17 +103,44 @@ describe('GET /api/logs/export', () => {
103103
)
104104
})
105105

106-
it('materializes one bounded page at a time while preserving CSV row order', async () => {
107-
queueTableRows(workflowExecutionLogs, [logRow(0), logRow(1), logRow(2)])
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+
)
108135

109136
const response = await GET(makeRequest())
110137
const lines = (await response.text()).trimEnd().split('\n')
111138

112139
expect(response.status).toBe(200)
113-
expect(mockMapWithConcurrency.mock.calls.map(([items]) => items.length)).toEqual([1, 1, 1])
114-
expect(lines).toHaveLength(4)
140+
expect(mockMapWithConcurrency.mock.calls.map(([items]) => items.length)).toEqual([20, 20, 5])
141+
expect(lines).toHaveLength(46)
115142
expect(lines[1]).toContain('execution-0')
116-
expect(lines.at(-1)).toContain('execution-2')
143+
expect(lines.at(-1)).toContain('execution-44')
117144
})
118145

119146
it('resumes full pages by startedAt and id without using OFFSET', async () => {

apps/sim/app/api/logs/export/route.ts

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getErrorMessage } from '@sim/utils/errors'
55
import { and, desc, eq, lt, or, type SQL, sql } from 'drizzle-orm'
66
import { type NextRequest, NextResponse } from 'next/server'
77
import { getSession } from '@/lib/auth'
8-
import { mapWithConcurrency } from '@/lib/core/utils/concurrency'
8+
import { MATERIALIZE_CONCURRENCY, mapWithConcurrency } from '@/lib/core/utils/concurrency'
99
import { formatCsvValue, toCsvRow } from '@/lib/core/utils/csv'
1010
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1111
import { materializeExecutionDataForDisplay } from '@/lib/logs/execution/trace-store'
@@ -15,7 +15,6 @@ import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
1515

1616
const logger = createLogger('LogsExportAPI')
1717
const LOG_EXPORT_PAGE_SIZE = 100
18-
const LOG_EXPORT_MATERIALIZE_CONCURRENCY = 1
1918

2019
export const revalidate = 0
2120

@@ -120,25 +119,18 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
120119

121120
if (!rows.length) break
122121

123-
for (
124-
let chunkStart = 0;
125-
chunkStart < rows.length;
126-
chunkStart += LOG_EXPORT_MATERIALIZE_CONCURRENCY
127-
) {
128-
const chunk = rows.slice(chunkStart, chunkStart + LOG_EXPORT_MATERIALIZE_CONCURRENCY)
129-
const materialized = await mapWithConcurrency(
130-
chunk,
131-
LOG_EXPORT_MATERIALIZE_CONCURRENCY,
132-
(row) =>
133-
materializeExecutionDataForDisplay(
134-
row.executionData as Record<string, unknown> | null,
135-
{
136-
workspaceId: params.workspaceId,
137-
workflowId: row.workflowId,
138-
executionId: row.executionId,
139-
userId: session.user.id,
140-
}
141-
)
122+
for (let chunkStart = 0; chunkStart < rows.length; chunkStart += MATERIALIZE_CONCURRENCY) {
123+
const chunk = rows.slice(chunkStart, chunkStart + MATERIALIZE_CONCURRENCY)
124+
const materialized = await mapWithConcurrency(chunk, MATERIALIZE_CONCURRENCY, (row) =>
125+
materializeExecutionDataForDisplay(
126+
row.executionData as Record<string, unknown> | null,
127+
{
128+
workspaceId: params.workspaceId,
129+
workflowId: row.workflowId,
130+
executionId: row.executionId,
131+
userId: session.user.id,
132+
}
133+
)
142134
)
143135

144136
for (let index = 0; index < chunk.length; index++) {

apps/sim/lib/billing/calculations/usage-monitor.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,29 @@ describe('checkUsageStatus', () => {
141141
expect(mockGetBillingPeriodUsageCost).not.toHaveBeenCalled()
142142
})
143143

144+
it('preserves the paid daily-refresh clamp for negative effective usage', async () => {
145+
const periodStart = new Date('2026-06-01T00:00:00.000Z')
146+
const periodEnd = new Date('2026-07-01T00:00:00.000Z')
147+
const subscription = {
148+
referenceId: 'user-1',
149+
plan: 'pro',
150+
status: 'active',
151+
seats: 1,
152+
periodStart,
153+
periodEnd,
154+
}
155+
dbChainMockFns.limit.mockResolvedValueOnce([{ currentPeriodCost: '0' }])
156+
mockComputeBillingPeriodUsageWithDailyRefresh.mockResolvedValueOnce({
157+
ledgerUsage: -1,
158+
refreshConsumed: 1,
159+
})
160+
161+
await expect(checkUsageStatus('user-1', subscription)).resolves.toMatchObject({
162+
currentUsage: 0,
163+
scope: 'user',
164+
})
165+
})
166+
144167
it('keeps unpaid personal usage on the ledger-only query', async () => {
145168
const periodStart = new Date('2026-06-01T00:00:00.000Z')
146169
const periodEnd = new Date('2026-07-01T00:00:00.000Z')
@@ -166,6 +189,28 @@ describe('checkUsageStatus', () => {
166189
expect(mockComputeBillingPeriodUsageWithDailyRefresh).not.toHaveBeenCalled()
167190
})
168191

192+
it('preserves negative ledger-only personal usage', async () => {
193+
const periodStart = new Date('2026-06-01T00:00:00.000Z')
194+
const periodEnd = new Date('2026-07-01T00:00:00.000Z')
195+
const subscription = {
196+
referenceId: 'user-1',
197+
plan: 'free',
198+
status: 'active',
199+
seats: 1,
200+
periodStart,
201+
periodEnd,
202+
}
203+
dbChainMockFns.limit.mockResolvedValueOnce([{ currentPeriodCost: '0' }])
204+
mockGetBillingPeriodUsageCost.mockResolvedValueOnce(-1)
205+
206+
await expect(checkUsageStatus('user-1', subscription)).resolves.toMatchObject({
207+
currentUsage: -1,
208+
scope: 'user',
209+
})
210+
211+
expect(mockComputeBillingPeriodUsageWithDailyRefresh).not.toHaveBeenCalled()
212+
})
213+
169214
it('combines paid organization ledger usage with bounded member refresh', async () => {
170215
const periodStart = new Date('2026-06-01T00:00:00.000Z')
171216
const periodEnd = new Date('2026-07-01T00:00:00.000Z')

apps/sim/lib/billing/calculations/usage-monitor.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export async function checkUsageStatus(
182182
: defaultBillingPeriod())
183183
let ledgerUsage: number
184184
let refreshConsumed = 0
185+
let appliedDailyRefresh = false
185186
if (sub && isPaid(sub.plan) && sub.periodStart) {
186187
const planDollars = getPlanTierDollars(sub.plan)
187188
if (planDollars > 0) {
@@ -195,16 +196,16 @@ export async function checkUsageStatus(
195196
})
196197
ledgerUsage = usage.ledgerUsage
197198
refreshConsumed = usage.refreshConsumed
199+
appliedDailyRefresh = true
198200
} else {
199201
ledgerUsage = await getBillingPeriodUsageCost({ type: 'user', id: userId }, billingPeriod)
200202
}
201203
} else {
202204
ledgerUsage = await getBillingPeriodUsageCost({ type: 'user', id: userId }, billingPeriod)
203205
}
204-
const currentUsage = Math.max(
205-
0,
206+
const usageBeforeRefresh =
206207
toNumber(toDecimal(statsRecords[0].currentPeriodCost)) + ledgerUsage - refreshConsumed
207-
)
208+
const currentUsage = appliedDailyRefresh ? Math.max(0, usageBeforeRefresh) : usageBeforeRefresh
208209

209210
return buildUsageData({ currentUsage, limit, scope, organizationId })
210211
} catch (error) {
Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
1-
-- migration-safe: tune the append-only ledger's vacuum thresholds without changing row data or query semantics.
1+
-- migration-safe: tune the append-heavy ledger's vacuum thresholds without changing row data or query semantics.
22
ALTER TABLE "usage_log" SET (autovacuum_vacuum_insert_scale_factor = 0.01, autovacuum_analyze_scale_factor = 0.01);--> statement-breakpoint
33
-- Concurrent index operations cannot run inside the migration runner's transaction.
44
COMMIT;--> statement-breakpoint
55
SET lock_timeout = 0;--> statement-breakpoint
66
-- migration-safe: replay removes an invalid build created by this migration; concurrent operations preserve writes.
7-
DROP INDEX CONCURRENTLY IF EXISTS "doc_active_kb_filename_idx";--> statement-breakpoint
8-
CREATE INDEX CONCURRENTLY IF NOT EXISTS "doc_active_kb_filename_idx" ON "document" USING btree ("knowledge_base_id","filename","uploaded_at" DESC,"token_count") WHERE "document"."user_excluded" = false AND "document"."archived_at" IS NULL AND "document"."deleted_at" IS NULL;--> statement-breakpoint
9-
-- migration-safe: replay removes an invalid build created by this migration; concurrent operations preserve writes.
10-
DROP INDEX CONCURRENTLY IF EXISTS "folder_active_workspace_resource_sort_idx";--> statement-breakpoint
11-
CREATE INDEX CONCURRENTLY IF NOT EXISTS "folder_active_workspace_resource_sort_idx" ON "folder" USING btree ("workspace_id","resource_type","sort_order","created_at") WHERE "folder"."deleted_at" IS NULL;--> statement-breakpoint
12-
-- migration-safe: the existing connector_id index remains available while this ordered replacement expands.
7+
DROP INDEX CONCURRENTLY IF EXISTS "doc_active_kb_token_count_idx";--> statement-breakpoint
8+
CREATE INDEX CONCURRENTLY IF NOT EXISTS "doc_active_kb_token_count_idx" ON "document" USING btree ("knowledge_base_id","token_count") WHERE "document"."user_excluded" = false AND "document"."archived_at" IS NULL AND "document"."deleted_at" IS NULL;--> statement-breakpoint
9+
-- migration-safe: the existing connector_id index remains available until the ordered replacement is valid.
1310
DROP INDEX CONCURRENTLY IF EXISTS "kcsl_connector_started_at_idx";--> statement-breakpoint
1411
CREATE INDEX CONCURRENTLY IF NOT EXISTS "kcsl_connector_started_at_idx" ON "knowledge_connector_sync_log" USING btree ("connector_id","started_at" DESC);--> statement-breakpoint
1512
-- migration-safe: replay removes an invalid build created by this migration; concurrent operations preserve writes.
@@ -18,17 +15,13 @@ CREATE INDEX CONCURRENTLY IF NOT EXISTS "table_views_workspace_created_idx" ON "
1815
-- migration-safe: replay removes an invalid build created by this migration; concurrent operations preserve writes.
1916
DROP INDEX CONCURRENTLY IF EXISTS "workflow_active_workspace_sort_idx";--> statement-breakpoint
2017
CREATE INDEX CONCURRENTLY IF NOT EXISTS "workflow_active_workspace_sort_idx" ON "workflow" USING btree ("workspace_id","sort_order","created_at","id") WHERE "workflow"."archived_at" IS NULL;--> statement-breakpoint
21-
-- migration-safe: replay removes an invalid build created by this migration; concurrent operations preserve writes.
22-
DROP INDEX CONCURRENTLY IF EXISTS "workflow_active_workspace_folder_sort_idx";--> statement-breakpoint
23-
CREATE INDEX CONCURRENTLY IF NOT EXISTS "workflow_active_workspace_folder_sort_idx" ON "workflow" USING btree ("workspace_id","folder_id","sort_order","created_at","id") WHERE "workflow"."archived_at" IS NULL;--> statement-breakpoint
24-
-- migration-safe: the existing workflow/time index remains available while the keyset replacement expands.
25-
DROP INDEX CONCURRENTLY IF EXISTS "workflow_execution_logs_workflow_started_at_id_idx";--> statement-breakpoint
26-
CREATE INDEX CONCURRENTLY IF NOT EXISTS "workflow_execution_logs_workflow_started_at_id_idx" ON "workflow_execution_logs" USING btree ("workflow_id","started_at","id");--> statement-breakpoint
27-
-- migration-safe: each removed index duplicates a primary or unique index with the identical leading key.
18+
-- migration-safe: each removed index duplicates a primary or unique index with the identical key definition.
2819
DROP INDEX CONCURRENTLY IF EXISTS "academy_certificate_number_idx";--> statement-breakpoint
2920
DROP INDEX CONCURRENTLY IF EXISTS "copilot_async_tool_calls_tool_call_id_idx";--> statement-breakpoint
3021
DROP INDEX CONCURRENTLY IF EXISTS "mothership_settings_workspace_id_idx";--> statement-breakpoint
3122
DROP INDEX CONCURRENTLY IF EXISTS "permissions_user_entity_idx";--> statement-breakpoint
3223
DROP INDEX CONCURRENTLY IF EXISTS "session_token_idx";--> statement-breakpoint
3324
DROP INDEX CONCURRENTLY IF EXISTS "workspace_file_key_idx";--> statement-breakpoint
25+
-- migration-safe: the valid ordered connector index covers the old index's complete key definition.
26+
DROP INDEX CONCURRENTLY IF EXISTS "kcsl_connector_id_idx";--> statement-breakpoint
3427
SET lock_timeout = '5s';

0 commit comments

Comments
 (0)