Skip to content

Commit 45f0a6c

Browse files
fix(db): drop displayName backfill, accept legacy NULLs
Make display_name nullable so the migration is a clean two-statement ADD COLUMN + CREATE UNIQUE INDEX with no backfill. Legacy rows keep display_name = NULL; readers coalesce to original_name. Pre-existing collisions persist (the upload already happened — no fix possible without rewriting history) but new uploads get full disambiguation via the partial unique index, which treats NULLs as distinct so legacy rows don't block index creation or new inserts. Replaces the prior 0202_hard_grey_gargoyle migration (window-function backfill). Resolver now matches WHERE display_name = ? OR (display_name IS NULL AND original_name = ?), and the normalize-segment fallback coalesces too.
1 parent 99128a3 commit 45f0a6c

7 files changed

Lines changed: 34 additions & 61 deletions

File tree

apps/sim/lib/copilot/tools/handlers/materialize-file.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ function toFileRecord(row: typeof workspaceFiles.$inferSelect) {
2121
return {
2222
id: row.id,
2323
workspaceId: row.workspaceId || '',
24-
name: row.displayName,
24+
name: row.displayName ?? row.originalName,
2525
key: row.key,
2626
path: `${pathPrefix}${encodeURIComponent(row.key)}?context=mothership`,
2727
size: row.size,
@@ -45,7 +45,7 @@ async function executeSave(fileName: string, chatId: string): Promise<ToolCallRe
4545

4646
const [updated] = await db
4747
.update(workspaceFiles)
48-
.set({ context: 'workspace', chatId: null, originalName: row.displayName })
48+
.set({ context: 'workspace', chatId: null, originalName: row.displayName ?? row.originalName })
4949
.where(and(eq(workspaceFiles.id, row.id), isNull(workspaceFiles.deletedAt)))
5050
.returning({ id: workspaceFiles.id, originalName: workspaceFiles.originalName })
5151

apps/sim/lib/copilot/tools/handlers/upload-file-reader.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,29 @@ import { db } from '@sim/db'
22
import { workspaceFiles } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { toError } from '@sim/utils/errors'
5-
import { and, asc, eq, isNull } from 'drizzle-orm'
5+
import { and, asc, eq, isNull, or } from 'drizzle-orm'
66
import { type FileReadResult, readFileRecord } from '@/lib/copilot/vfs/file-reader'
77
import { normalizeVfsSegment } from '@/lib/copilot/vfs/normalize-segment'
88
import { getServePathPrefix } from '@/lib/uploads'
99
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
1010

1111
const logger = createLogger('UploadFileReader')
1212

13+
/**
14+
* VFS-visible name. Pre-displayName-column rows have NULL displayName, so we coalesce
15+
* to originalName for them (those rows may still collide with each other — acceptable
16+
* since the upload already happened; only new rows get collision protection).
17+
*/
18+
function vfsName(row: typeof workspaceFiles.$inferSelect): string {
19+
return row.displayName ?? row.originalName
20+
}
21+
1322
function toWorkspaceFileRecord(row: typeof workspaceFiles.$inferSelect): WorkspaceFileRecord {
1423
const pathPrefix = getServePathPrefix()
1524
return {
1625
id: row.id,
1726
workspaceId: row.workspaceId || '',
18-
name: row.displayName,
27+
name: vfsName(row),
1928
key: row.key,
2029
path: `${pathPrefix}${encodeURIComponent(row.key)}?context=mothership`,
2130
size: row.size,
@@ -29,10 +38,10 @@ function toWorkspaceFileRecord(row: typeof workspaceFiles.$inferSelect): Workspa
2938
}
3039

3140
/**
32-
* Resolve a mothership upload row by `displayName` (the collision-disambiguated name
33-
* exposed via `uploads/<displayName>` in the VFS). Prefers an exact DB match; falls back
34-
* to a normalized scan when the model passes a visually-equivalent name (e.g. macOS U+202F
35-
* narrow no-break space vs ASCII space in screenshot filenames).
41+
* Resolve a mothership upload row by VFS name (the collision-disambiguated `displayName`
42+
* for new rows, or `originalName` for legacy rows that predate the column). Prefers an
43+
* exact DB match; falls back to a normalized scan when the model passes a visually
44+
* equivalent name (e.g. macOS U+202F vs ASCII space in screenshot filenames).
3645
*/
3746
export async function findMothershipUploadRowByChatAndName(
3847
chatId: string,
@@ -45,7 +54,10 @@ export async function findMothershipUploadRowByChatAndName(
4554
and(
4655
eq(workspaceFiles.chatId, chatId),
4756
eq(workspaceFiles.context, 'mothership'),
48-
eq(workspaceFiles.displayName, fileName),
57+
or(
58+
eq(workspaceFiles.displayName, fileName),
59+
and(isNull(workspaceFiles.displayName), eq(workspaceFiles.originalName, fileName))
60+
),
4961
isNull(workspaceFiles.deletedAt)
5062
)
5163
)
@@ -67,7 +79,7 @@ export async function findMothershipUploadRowByChatAndName(
6779
)
6880

6981
const segmentKey = normalizeVfsSegment(fileName)
70-
return allRows.find((r) => normalizeVfsSegment(r.displayName) === segmentKey) ?? null
82+
return allRows.find((r) => normalizeVfsSegment(vfsName(r)) === segmentKey) ?? null
7183
}
7284

7385
/**

packages/db/migrations/0202_hard_grey_gargoyle.sql

Lines changed: 0 additions & 44 deletions
This file was deleted.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE "workspace_files" ADD COLUMN "display_name" text;--> statement-breakpoint
2+
CREATE UNIQUE INDEX "workspace_files_chat_display_name_active_unique" ON "workspace_files" USING btree ("chat_id","display_name") WHERE "workspace_files"."deleted_at" IS NULL AND "workspace_files"."context" = 'mothership' AND "workspace_files"."chat_id" IS NOT NULL;

packages/db/migrations/meta/0202_snapshot.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"id": "549270ba-08fa-4690-af38-84494e559936",
2+
"id": "0440b68d-47b7-4104-af1b-15f3206e9d7c",
33
"prevId": "f7dc3eab-9b08-4ed7-99b0-d952797d86eb",
44
"version": "7",
55
"dialect": "postgresql",
@@ -14513,7 +14513,7 @@
1451314513
"name": "display_name",
1451414514
"type": "text",
1451514515
"primaryKey": false,
14516-
"notNull": true
14516+
"notNull": false
1451714517
},
1451814518
"content_type": {
1451914519
"name": "content_type",

packages/db/migrations/meta/_journal.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,8 +1412,8 @@
14121412
{
14131413
"idx": 202,
14141414
"version": "7",
1415-
"when": 1778009467652,
1416-
"tag": "0202_hard_grey_gargoyle",
1415+
"when": 1778014005108,
1416+
"tag": "0202_superb_abomination",
14171417
"breakpoints": true
14181418
}
14191419
]

packages/db/schema.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,10 +1149,13 @@ export const workspaceFiles = pgTable(
11491149
/**
11501150
* Collision-disambiguated name exposed to the copilot VFS as `uploads/<displayName>`.
11511151
* For mothership chat uploads, identical originalNames within a chat get suffixed
1152-
* `(2)`, `(3)`, ... in upload order. For other contexts, equals originalName.
1153-
* Stable for the row's lifetime; the partial unique index below makes that enforceable.
1152+
* `(2)`, `(3)`, ... in upload order so the VFS path is unique per chat.
1153+
* NULL on legacy rows that predate this column — readers must coalesce to originalName.
1154+
* Stable for the row's lifetime; the partial unique index below enforces uniqueness
1155+
* for new (non-NULL) rows. NULLs are treated as distinct in PG unique indexes, so
1156+
* legacy collisions remain (acceptable: those uploads have already happened).
11541157
*/
1155-
displayName: text('display_name').notNull(),
1158+
displayName: text('display_name'),
11561159
contentType: text('content_type').notNull(),
11571160
size: integer('size').notNull(),
11581161
deletedAt: timestamp('deleted_at'),

0 commit comments

Comments
 (0)