Skip to content

Commit a4bfd13

Browse files
committed
chore(file-search): retire legacy index storage
1 parent eaa9ff0 commit a4bfd13

9 files changed

Lines changed: 27830 additions & 211 deletions

File tree

‎apps/sim/lib/workspace-files/search/README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ Search results retain `fileId`, 1-based `lineNumber`, and bounded `text` preview
4343
1. Deploy the additive migration and new application/Trigger worker versions. Legacy index tables remain readable for the old deployment. The file trigger queues current revisions in the new table; this cutover intentionally allows temporary search unavailability while the new index builds.
4444
2. The dispatcher uses a separate `workspace-file-search-chunks-v2` backfill cursor. It seeds at most 1,000 active files per pass under a shared file lock, with idempotent inserts. Normal dispatch caps remain two outstanding jobs per workspace, 100 outstanding globally, and ten running workers. Reconciliation repeats hourly after a complete pass to repair missing metadata. Failed revisions remain visible as failed; they are not silently declared covered.
4545
3. Before retiring legacy storage, verify the new app and Trigger workers are fully deployed, old runs/retries have drained, the backfill cursor has completed, and scoped coverage is ready or explicitly excluded. Investigate failed or stale pending revisions. Check cleanup backlog and run representative exact/regex searches, including long lines and folder scopes.
46-
4. After the rollback window, ship a separate contract PR removing the legacy schema and dropping `workspace_file_search_segment` / `workspace_file_search_index` with a short lock timeout. Do not delete the entire old index row-by-row or backfill it inside the schema migration. Dropping obsolete tables reclaims their heap, indexes, and TOAST together. The `contract-pending` marker in `packages/db/schema.ts` tracks this step.
46+
4. After the rollback window and the checks above, deploy `0368_retire_legacy_file_search.sql`. It drops only `workspace_file_search_segment` and `workspace_file_search_index`, atomically, with a two-second lock timeout and without `CASCADE`. An unexpected dependency or lock conflict aborts the migration instead of removing dependent objects. Replay tolerates tables already retired. Dropping the tables reclaims their heap, indexes, and TOAST together; it does not delete current chunks or rebuild search.
4747

48-
Until that contract deploy, legacy foreign-key cascades can still make a hard file/workspace deletion expensive. New-index cleanup is bounded; retaining the old schema cannot erase that legacy cost. The earlier timestamp-repair script detects the chunk schema and leaves obsolete legacy text for this contract step instead of deleting it in bulk. Legacy-table retirement remains a separate contract migration.
48+
The status enum, dispatch queue, and v2 backfill cursor remain in use. The timestamp-repair script still preserves provenance and repairs current revisions; its legacy deletion branch has been removed. No bulk deletion or replacement backfill runs in the contract migration.
4949

50-
Rollback before retirement requires restoring the old trigger function as well as the old app/worker version, and reconciling legacy revisions written during the cutover. Do not assume retained tables are automatically up to date. Canonical revision joins prevent stale content from being returned.
50+
After retirement, rollback must stay on a chunk-compatible app and worker release. Restoring the old segment-based implementation requires recreating and rebuilding its retired storage; reverting application code alone is insufficient.
5151

5252
### Direct GIN writes
5353

‎apps/sim/lib/workspace-files/search/chunks.integration.ts‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ describe('chunked workspace file search on PostgreSQL', () => {
187187
'0358_workspace_file_content_version_precision.sql',
188188
'0359_workspace_file_search_chunks.sql',
189189
ginWriteMigration,
190+
'0368_retire_legacy_file_search.sql',
190191
]) {
191192
await applyMigration(migration)
192193
}
@@ -208,7 +209,7 @@ describe('chunked workspace file search on PostgreSQL', () => {
208209
})
209210
beforeEach(async () => {
210211
await connection`TRUNCATE workspace, workspace_files, workspace_file_search_revision, workspace_file_search_build,
211-
workspace_file_search_chunk, workspace_file_search_index, workspace_file_search_segment, workspace_file_search_dispatch_queue, workspace_file_search_backfill`
212+
workspace_file_search_chunk, workspace_file_search_dispatch_queue, workspace_file_search_backfill`
212213
await connection`INSERT INTO workspace_file_search_backfill (id, completed_at) VALUES ('workspace-file-search-chunks-v2', now())`
213214
await addFile('file-1')
214215
})
@@ -222,6 +223,45 @@ describe('chunked workspace file search on PostgreSQL', () => {
222223
}
223224
})
224225

226+
it('retires legacy tables atomically, preserves current search, and safely replays', async () => {
227+
await index('heading\nretirement needle\ntail')
228+
await connection`CREATE TABLE workspace_file_search_index (file_id text PRIMARY KEY)`
229+
await connection`CREATE TABLE workspace_file_search_segment (file_id text, content text)`
230+
await connection`INSERT INTO workspace_file_search_index VALUES ('retired-file')`
231+
await connection`INSERT INTO workspace_file_search_segment VALUES ('retired-file', 'retired text')`
232+
await connection`CREATE VIEW legacy_dependency AS SELECT * FROM workspace_file_search_index`
233+
try {
234+
await expect(applyMigration('0368_retire_legacy_file_search.sql')).rejects.toMatchObject({
235+
code: '2BP01',
236+
})
237+
expect(
238+
(await connection`SELECT count(*)::int AS count FROM workspace_file_search_segment`)[0]
239+
.count
240+
).toBe(1)
241+
await connection`DROP VIEW legacy_dependency`
242+
for (let attempt = 0; attempt < 2; attempt++) {
243+
await applyMigration('0368_retire_legacy_file_search.sql')
244+
expect(
245+
(
246+
await connection`SELECT to_regclass('workspace_file_search_index') AS legacy_index,
247+
to_regclass('workspace_file_search_segment') AS legacy_segment`
248+
)[0]
249+
).toEqual({ legacy_index: null, legacy_segment: null })
250+
expect((await search('retirement needle')).results).toMatchObject([
251+
{ fileId: 'file-1', lineNumber: 2 },
252+
])
253+
expect((await search('^retirement.*needle$', 'regex')).results).toMatchObject([
254+
{ fileId: 'file-1', lineNumber: 2 },
255+
])
256+
}
257+
} finally {
258+
await connection`DROP VIEW IF EXISTS legacy_dependency`
259+
await applyMigration('0368_retire_legacy_file_search.sql')
260+
}
261+
await connection`DELETE FROM workspace_files WHERE id = 'file-1'`
262+
expect((await search('needle')).results).toEqual([])
263+
})
264+
225265
it('preserves search through disabling, draining, and replaying GIN pending-list maintenance', async () => {
226266
await connection`ALTER INDEX workspace_file_search_chunk_content_idx SET (fastupdate = on)`
227267
try {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
SET LOCAL lock_timeout = '2s';
2+
--> statement-breakpoint
3+
-- migration-safe: contract of #7947; deployed app, workers, and revision triggers use chunk storage. Retire only after the rollback window and completed backfill verification.
4+
DROP TABLE IF EXISTS "workspace_file_search_segment";
5+
--> statement-breakpoint
6+
-- migration-safe: contract of #7947; current revision metadata lives in workspace_file_search_revision, with no remaining runtime reader of this legacy table.
7+
DROP TABLE IF EXISTS "workspace_file_search_index";

0 commit comments

Comments
 (0)