|
| 1 | +/** |
| 2 | + * The plans the paged file reads must get from the live workspace-file indexes, proven on the |
| 3 | + * exact SQL the managers send: a workspace-wide page by upload date streams |
| 4 | + * `workspace_files_workspace_active_uploaded_idx`, a folder page streams |
| 5 | + * `workspace_files_workspace_folder_uploaded_idx`, a name-sorted folder page seeks the folder-name |
| 6 | + * index, a folder rollup seeks a folder-keyed index, and a name search probes |
| 7 | + * `workspace_files_workspace_name_trgm_idx`. |
| 8 | + */ |
| 9 | +import { db } from '@sim/db' |
| 10 | +import { permissions, user, workspace, workspaceFiles } from '@sim/db/schema' |
| 11 | +import { explainCaptured, type PlanNode } from '@sim/db/testing/explain' |
| 12 | +import { assertDisposableTestDatabaseUrl } from '@sim/db/testing/test-infrastructure' |
| 13 | +import { deleteWorkspaceFixture, insertWorkspaceFixture } from '@sim/db/testing/workspace-fixtures' |
| 14 | +import { generateId } from '@sim/utils/id' |
| 15 | +import { eq, sql } from 'drizzle-orm' |
| 16 | +import { afterAll, beforeAll, describe, expect, it } from 'vitest' |
| 17 | +import { |
| 18 | + aggregateWorkspaceFileFolderUsage, |
| 19 | + queryWorkspaceFiles, |
| 20 | +} from '@/lib/uploads/contexts/workspace/file-manager' |
| 21 | + |
| 22 | +const alice = generateId() |
| 23 | +const workspaceId = generateId() |
| 24 | +const folderId = generateId() |
| 25 | + |
| 26 | +const scanOf = (nodes: PlanNode[], index: string) => |
| 27 | + nodes.find((node) => node['Index Name'] === index) |
| 28 | +const sorts = (nodes: PlanNode[]) => |
| 29 | + nodes.filter((node) => node['Node Type'] === 'Sort' || node['Node Type'] === 'Incremental Sort') |
| 30 | + |
| 31 | +describe('paged file read plans in PostgreSQL', () => { |
| 32 | + beforeAll(async () => { |
| 33 | + assertDisposableTestDatabaseUrl(process.env.DATABASE_URL ?? '') |
| 34 | + await db.insert(user).values({ |
| 35 | + id: alice, |
| 36 | + name: 'Plan fixture', |
| 37 | + email: `${alice}@fixture.test`, |
| 38 | + emailVerified: true, |
| 39 | + createdAt: new Date(), |
| 40 | + updatedAt: new Date(), |
| 41 | + }) |
| 42 | + await insertWorkspaceFixture(db, { |
| 43 | + id: workspaceId, |
| 44 | + name: 'Plan fixture', |
| 45 | + ownerId: alice, |
| 46 | + billedAccountUserId: alice, |
| 47 | + }) |
| 48 | + await db.insert(permissions).values({ |
| 49 | + id: generateId(), |
| 50 | + userId: alice, |
| 51 | + entityType: 'workspace', |
| 52 | + entityId: workspaceId, |
| 53 | + permissionType: 'admin', |
| 54 | + }) |
| 55 | + await db.execute(sql` |
| 56 | + INSERT INTO folder (id, name, user_id, workspace_id, resource_type) |
| 57 | + VALUES (${folderId}, 'Plans', ${alice}, ${workspaceId}, 'file')`) |
| 58 | + await db.execute(sql` |
| 59 | + INSERT INTO ${workspaceFiles} (id, key, user_id, workspace_id, folder_id, context, original_name, |
| 60 | + content_type, size_bytes, uploaded_at) |
| 61 | + SELECT 'wf_plan_' || n || '_' || ${workspaceId}, 'plan/' || n || '/' || ${workspaceId}, ${alice}, |
| 62 | + ${workspaceId}, CASE WHEN n % 3 = 0 THEN ${folderId} END, 'workspace', |
| 63 | + 'file-' || md5(n::text) || '.txt', 'text/plain', n, |
| 64 | + timestamp '2026-01-01' + n * interval '1 second' |
| 65 | + FROM generate_series(1, 6000) AS n`) |
| 66 | + await db.execute(sql`VACUUM ANALYZE ${workspaceFiles}`) |
| 67 | + }) |
| 68 | + |
| 69 | + afterAll(async () => { |
| 70 | + await deleteWorkspaceFixture(db, eq(workspace.id, workspaceId)) |
| 71 | + await db.delete(user).where(eq(user.id, alice)) |
| 72 | + }) |
| 73 | + |
| 74 | + const uploadedPage = (folder: string | null | undefined, sortOrder: 'asc' | 'desc') => |
| 75 | + explainCaptured( |
| 76 | + db, |
| 77 | + () => |
| 78 | + queryWorkspaceFiles(workspaceId, { |
| 79 | + folderId: folder, |
| 80 | + sortBy: 'uploadedAt', |
| 81 | + sortOrder, |
| 82 | + limit: 50, |
| 83 | + after: ['2026-01-01T00:30:00.000000Z', 'wf_plan_x'], |
| 84 | + }), |
| 85 | + { match: /order by/ } |
| 86 | + ) |
| 87 | + |
| 88 | + it.each(['asc', 'desc'] as const)( |
| 89 | + 'streams a workspace-wide page by upload date (%s) from the active upload index, without sorting', |
| 90 | + async (sortOrder) => { |
| 91 | + const nodes = await uploadedPage(undefined, sortOrder) |
| 92 | + const scan = scanOf(nodes, 'workspace_files_workspace_active_uploaded_idx') |
| 93 | + expect(scan?.['Index Cond']).toMatch(/uploaded_at [<>]=/) |
| 94 | + expect(sorts(nodes)).toEqual([]) |
| 95 | + } |
| 96 | + ) |
| 97 | + |
| 98 | + it.each([ |
| 99 | + ['the root', null, 'asc'], |
| 100 | + ['the root', null, 'desc'], |
| 101 | + ['a folder', folderId, 'asc'], |
| 102 | + ['a folder', folderId, 'desc'], |
| 103 | + ] as const)( |
| 104 | + 'streams a page of %s by upload date (%s) from the folder upload index, without sorting', |
| 105 | + async (_label, folder, sortOrder) => { |
| 106 | + const nodes = await uploadedPage(folder, sortOrder) |
| 107 | + const scan = scanOf(nodes, 'workspace_files_workspace_folder_uploaded_idx') |
| 108 | + expect(scan?.['Index Cond']).toMatch(/COALESCE\(folder_id.*uploaded_at [<>]=/) |
| 109 | + expect(sorts(nodes)).toEqual([]) |
| 110 | + } |
| 111 | + ) |
| 112 | + |
| 113 | + /** |
| 114 | + * Either folder-keyed index serves the rollup's seek — this one or the folder-name unique |
| 115 | + * index, which leads with the same `(workspace_id, coalesce(folder_id, ''))` — and the planner |
| 116 | + * picks between them on cost; what must never happen is a scan of the workspace's files. |
| 117 | + */ |
| 118 | + it('rolls folder usage up by seeking a folder-keyed index', async () => { |
| 119 | + let usage: Awaited<ReturnType<typeof aggregateWorkspaceFileFolderUsage>> | undefined |
| 120 | + const nodes = await explainCaptured(db, async () => { |
| 121 | + usage = await aggregateWorkspaceFileFolderUsage(workspaceId, [folderId]) |
| 122 | + }) |
| 123 | + expect(usage?.get(folderId)?.fileCount).toBe(2000) |
| 124 | + const seek = nodes.find((node) => /COALESCE\(folder_id/.test(node['Index Cond'] ?? '')) |
| 125 | + expect([ |
| 126 | + 'workspace_files_workspace_folder_uploaded_idx', |
| 127 | + 'workspace_files_workspace_folder_name_active_unique', |
| 128 | + ]).toContain(seek?.['Index Name']) |
| 129 | + }) |
| 130 | + |
| 131 | + /** |
| 132 | + * The folder predicate must be spelled on `coalesce(folder_id, '')`: that is the key the |
| 133 | + * `(workspace_id, coalesce(folder_id, ''), original_name)` index leads with, and a name-sorted |
| 134 | + * folder page that cannot seek it reads every live file the workspace holds. |
| 135 | + */ |
| 136 | + it.each([ |
| 137 | + ['the root', null], |
| 138 | + ['a folder', folderId], |
| 139 | + ])('seeks the folder name index for a name-sorted page of %s', async (_label, folder) => { |
| 140 | + const nodes = await explainCaptured( |
| 141 | + db, |
| 142 | + () => |
| 143 | + queryWorkspaceFiles(workspaceId, { |
| 144 | + folderId: folder, |
| 145 | + sortBy: 'name', |
| 146 | + sortOrder: 'asc', |
| 147 | + limit: 50, |
| 148 | + after: ['file-8', 'wf_plan_x'], |
| 149 | + }), |
| 150 | + { match: /order by/ } |
| 151 | + ) |
| 152 | + const scan = scanOf(nodes, 'workspace_files_workspace_folder_name_active_unique') |
| 153 | + expect(scan?.['Index Cond']).toMatch(/COALESCE\(folder_id.*original_name >=/) |
| 154 | + }) |
| 155 | + |
| 156 | + it('builds the trigram name index with fastupdate off', async () => { |
| 157 | + const [index] = await db.execute<{ options: string | null }>(sql` |
| 158 | + SELECT array_to_string(reloptions, ',') AS options FROM pg_class |
| 159 | + WHERE relname = 'workspace_files_workspace_name_trgm_idx'`) |
| 160 | + expect(index?.options).toBe('fastupdate=off') |
| 161 | + }) |
| 162 | + |
| 163 | + it('probes the trigram name index for a substring search', async () => { |
| 164 | + const nodes = await explainCaptured( |
| 165 | + db, |
| 166 | + () => |
| 167 | + queryWorkspaceFiles(workspaceId, { |
| 168 | + search: 'c4ca42', |
| 169 | + sortBy: 'name', |
| 170 | + sortOrder: 'asc', |
| 171 | + limit: 20, |
| 172 | + }), |
| 173 | + { match: /order by/ } |
| 174 | + ) |
| 175 | + expect(scanOf(nodes, 'workspace_files_workspace_name_trgm_idx')?.['Index Cond']).toMatch( |
| 176 | + /original_name ~~\* '%c4ca42%'/ |
| 177 | + ) |
| 178 | + }) |
| 179 | +}) |
0 commit comments