@@ -3,6 +3,7 @@ import { db } from '@sim/db'
33import * as schema from '@sim/db/schema'
44import { createLogger } from '@sim/logger'
55import { and , eq , notExists , or , sql } from 'drizzle-orm'
6+ import type { AnyPgColumn , PgTable } from 'drizzle-orm/pg-core'
67import type { NextRequest } from 'next/server'
78import {
89 type ResourceOwner ,
@@ -216,23 +217,16 @@ async function clearInWorkflowBlocks(
216217 workspaceId : string ,
217218 needle : string
218219) : Promise < void > {
219- const rows = await db
220- . select ( {
221- id : schema . workflowBlocks . id ,
222- subBlocks : schema . workflowBlocks . subBlocks ,
223- } )
224- . from ( schema . workflowBlocks )
225- . innerJoin ( schema . workflow , eq ( schema . workflow . id , schema . workflowBlocks . workflowId ) )
226- . where (
227- and (
228- eq ( schema . workflow . workspaceId , workspaceId ) ,
229- sql `${ schema . workflowBlocks . subBlocks } ::text LIKE ${ needle } `
230- )
231- )
220+ const rows = await readWorkspaceCredentialRefs ( workspaceId , needle , {
221+ table : schema . workflowBlocks ,
222+ id : schema . workflowBlocks . id ,
223+ workflowId : schema . workflowBlocks . workflowId ,
224+ value : schema . workflowBlocks . subBlocks ,
225+ } )
232226
233227 let updated = 0
234228 for ( const row of rows ) {
235- const next = clearCredentialInValue ( row . subBlocks , credentialId )
229+ const next = clearCredentialInValue ( row . value , credentialId )
236230 if ( next . changed ) {
237231 await db
238232 . update ( schema . workflowBlocks )
@@ -255,22 +249,15 @@ async function clearInDeploymentVersions(
255249 workspaceId : string ,
256250 needle : string
257251) : Promise < void > {
258- const rows = await db
259- . select ( {
260- id : schema . workflowDeploymentVersion . id ,
261- state : schema . workflowDeploymentVersion . state ,
262- } )
263- . from ( schema . workflowDeploymentVersion )
264- . innerJoin ( schema . workflow , eq ( schema . workflow . id , schema . workflowDeploymentVersion . workflowId ) )
265- . where (
266- and (
267- eq ( schema . workflow . workspaceId , workspaceId ) ,
268- sql `${ schema . workflowDeploymentVersion . state } ::text LIKE ${ needle } `
269- )
270- )
252+ const rows = await readWorkspaceCredentialRefs ( workspaceId , needle , {
253+ table : schema . workflowDeploymentVersion ,
254+ id : schema . workflowDeploymentVersion . id ,
255+ workflowId : schema . workflowDeploymentVersion . workflowId ,
256+ value : schema . workflowDeploymentVersion . state ,
257+ } )
271258
272259 for ( const row of rows ) {
273- const next = clearCredentialInValue ( row . state , credentialId )
260+ const next = clearCredentialInValue ( row . value , credentialId )
274261 if ( next . changed ) {
275262 await db
276263 . update ( schema . workflowDeploymentVersion )
@@ -285,22 +272,15 @@ async function clearInPausedExecutions(
285272 workspaceId : string ,
286273 needle : string
287274) : Promise < void > {
288- const rows = await db
289- . select ( {
290- id : schema . pausedExecutions . id ,
291- executionSnapshot : schema . pausedExecutions . executionSnapshot ,
292- } )
293- . from ( schema . pausedExecutions )
294- . innerJoin ( schema . workflow , eq ( schema . workflow . id , schema . pausedExecutions . workflowId ) )
295- . where (
296- and (
297- eq ( schema . workflow . workspaceId , workspaceId ) ,
298- sql `${ schema . pausedExecutions . executionSnapshot } ::text LIKE ${ needle } `
299- )
300- )
275+ const rows = await readWorkspaceCredentialRefs ( workspaceId , needle , {
276+ table : schema . pausedExecutions ,
277+ id : schema . pausedExecutions . id ,
278+ workflowId : schema . pausedExecutions . workflowId ,
279+ value : schema . pausedExecutions . executionSnapshot ,
280+ } )
301281
302282 for ( const row of rows ) {
303- const next = clearCredentialInValue ( row . executionSnapshot , credentialId )
283+ const next = clearCredentialInValue ( row . value , credentialId )
304284 if ( next . changed ) {
305285 await db
306286 . update ( schema . pausedExecutions )
@@ -315,22 +295,15 @@ async function clearInWorkflowCheckpoints(
315295 workspaceId : string ,
316296 needle : string
317297) : Promise < void > {
318- const rows = await db
319- . select ( {
320- id : schema . workflowCheckpoints . id ,
321- workflowState : schema . workflowCheckpoints . workflowState ,
322- } )
323- . from ( schema . workflowCheckpoints )
324- . innerJoin ( schema . workflow , eq ( schema . workflow . id , schema . workflowCheckpoints . workflowId ) )
325- . where (
326- and (
327- eq ( schema . workflow . workspaceId , workspaceId ) ,
328- sql `${ schema . workflowCheckpoints . workflowState } ::text LIKE ${ needle } `
329- )
330- )
298+ const rows = await readWorkspaceCredentialRefs ( workspaceId , needle , {
299+ table : schema . workflowCheckpoints ,
300+ id : schema . workflowCheckpoints . id ,
301+ workflowId : schema . workflowCheckpoints . workflowId ,
302+ value : schema . workflowCheckpoints . workflowState ,
303+ } )
331304
332305 for ( const row of rows ) {
333- const next = clearCredentialInValue ( row . workflowState , credentialId )
306+ const next = clearCredentialInValue ( row . value , credentialId )
334307 if ( next . changed ) {
335308 await db
336309 . update ( schema . workflowCheckpoints )
@@ -340,6 +313,29 @@ async function clearInWorkflowCheckpoints(
340313 }
341314}
342315
316+ /**
317+ * Restrict the rows before inspecting their JSON. With a plain join, Postgres can push
318+ * the text predicate below the workspace join and detoast every tenant's snapshots.
319+ * This query has reached 46s in production. Materializing the workspace selection
320+ * keeps the expensive scan local, including archived workflows
321+ * whose frozen snapshots still need their credential references removed.
322+ */
323+ async function readWorkspaceCredentialRefs (
324+ workspaceId : string ,
325+ needle : string ,
326+ source : { table : PgTable ; id : AnyPgColumn ; workflowId : AnyPgColumn ; value : AnyPgColumn }
327+ ) : Promise < Array < { id : string ; value : unknown } > > {
328+ return db . execute < { id : string ; value : unknown } > ( sql `
329+ WITH workspace_credential_refs AS MATERIALIZED (
330+ SELECT ${ source . id } AS id, ${ source . value } AS value
331+ FROM ${ source . table }
332+ INNER JOIN ${ schema . workflow } ON ${ schema . workflow . id } = ${ source . workflowId }
333+ WHERE ${ schema . workflow . workspaceId } = ${ workspaceId }
334+ )
335+ SELECT id, value FROM workspace_credential_refs WHERE value::text LIKE ${ needle }
336+ ` )
337+ }
338+
343339async function clearInKnowledgeConnectors ( credentialId : string ) : Promise < void > {
344340 await db
345341 . update ( schema . knowledgeConnector )
0 commit comments