@@ -21,7 +21,7 @@ import { writeWorkflowGroupState } from '@/lib/table/cell-write'
2121import { USER_TABLE_ROWS_SQL_NAME } from '@/lib/table/constants'
2222import { isExecCancelledAfter } from '@/lib/table/deps'
2323import { appendTableEvent } from '@/lib/table/events'
24- import { type DbExecutor , withSeqscanOff } from '@/lib/table/planner'
24+ import { type DbExecutor , type DbTransaction , withSeqscanOff } from '@/lib/table/planner'
2525import { updateTableRowsWithDerivedSecretProvenance } from '@/lib/table/rows/secret-provenance'
2626import { buildFilterClause } from '@/lib/table/sql'
2727import type {
@@ -87,6 +87,24 @@ export interface DispatchRow {
8787 requestedAt : Date
8888}
8989
90+ async function deleteExecutionRows ( trx : DbTransaction , filters : SQL [ ] ) : Promise < number > {
91+ const countRows = await trx . execute < { count : number | string } > ( sql `
92+ WITH deleted AS (
93+ DELETE FROM ${ tableRowExecutions }
94+ WHERE ${ and ( ...filters ) }
95+ RETURNING 1
96+ )
97+ SELECT count(*)::integer AS count FROM deleted
98+ ` )
99+ const [ countRow ] = Array . isArray ( countRows ) ? countRows : [ ]
100+ if ( ! countRow ) throw new Error ( 'Workflow cell clearing did not return a deleted count' )
101+ const count = Number ( countRow . count )
102+ if ( ! Number . isSafeInteger ( count ) || count < 0 ) {
103+ throw new Error ( 'Workflow cell clearing returned an invalid deleted count' )
104+ }
105+ return count
106+ }
107+
90108export type DispatcherStepResult = 'continue' | 'done'
91109
92110/** Eager bulk clear at click time so the user sees every targeted cell go
@@ -96,17 +114,18 @@ export type DispatcherStepResult = 'continue' | 'done'
96114 * already filled, mirroring the eligibility predicate. */
97115export async function bulkClearWorkflowGroupCells ( input : {
98116 tableId : string
117+ workspaceId : string
99118 groups : Array < { id : string ; outputs : Array < { columnName : string } > } >
100119 rowIds ?: string [ ]
101120 /** Select-all scope: deselected rows whose outputs must NOT be wiped. */
102121 excludeRowIds ?: string [ ]
103122 mode : DispatchMode
104- } ) : Promise < void > {
105- const { tableId, groups, rowIds, excludeRowIds, mode } = input
106- if ( groups . length === 0 ) return
123+ } ) : Promise < boolean > {
124+ const { tableId, workspaceId , groups, rowIds, excludeRowIds, mode } = input
125+ if ( groups . length === 0 ) return false
107126 // `'new'` mode targets only rows with no prior attempt — nothing to clear.
108127 // Pre-existing outputs on any other row must not be wiped by an auto-fire.
109- if ( mode === 'new' ) return
128+ if ( mode === 'new' ) return false
110129
111130 const groupIds = groups . map ( ( g ) => g . id )
112131 const rowScope = rowIds && rowIds . length > 0 ? rowIds : null
@@ -119,25 +138,34 @@ export async function bulkClearWorkflowGroupCells(input: {
119138 const outputCols = Array . from (
120139 new Set ( groups . flatMap ( ( g ) => g . outputs . map ( ( o ) => o . columnName ) ) )
121140 )
122- const filters : SQL [ ] = [ eq ( userTableRows . tableId , tableId ) ]
141+ const filters : SQL [ ] = [
142+ eq ( userTableRows . tableId , tableId ) ,
143+ eq ( userTableRows . workspaceId , workspaceId ) ,
144+ ]
123145 if ( rowScope ) filters . push ( inArray ( userTableRows . id , rowScope ) )
124146 if ( excluded ) filters . push ( notInArray ( userTableRows . id , excluded ) )
125147
126- await db . transaction ( async ( trx ) => {
148+ return db . transaction ( async ( trx ) => {
127149 const rowWhere = and ( ...filters ) !
128- await updateTableRowsWithDerivedSecretProvenance ( trx , {
150+ const clearedRows = await updateTableRowsWithDerivedSecretProvenance ( trx , {
129151 rowWhere,
130152 transformation : { mode : 'remove-columns' , columnIds : outputCols } ,
131153 } )
132154 const execFilters : SQL [ ] = [
133155 eq ( tableRowExecutions . tableId , tableId ) ,
134156 inArray ( tableRowExecutions . groupId , groupIds ) ,
157+ sql `${ tableRowExecutions . rowId } IN (
158+ SELECT ${ userTableRows . id }
159+ FROM ${ userTableRows }
160+ WHERE ${ userTableRows . tableId } = ${ tableId }
161+ AND ${ userTableRows . workspaceId } = ${ workspaceId }
162+ )` ,
135163 ]
136164 if ( rowScope ) execFilters . push ( inArray ( tableRowExecutions . rowId , rowScope ) )
137165 if ( excluded ) execFilters . push ( notInArray ( tableRowExecutions . rowId , excluded ) )
138- await trx . delete ( tableRowExecutions ) . where ( and ( ...execFilters ) )
166+ const deletedExecutions = await deleteExecutionRows ( trx , execFilters )
167+ return clearedRows > 0 || deletedExecutions > 0
139168 } )
140- return
141169 }
142170
143171 // `incomplete`: clear per-group, not per-row. Only groups that are
@@ -147,20 +175,25 @@ export async function bulkClearWorkflowGroupCells(input: {
147175 // because a *sibling* group on the same row is incomplete, re-running the
148176 // completed one. (`never-run` groups have no exec/output to clear — the
149177 // dispatcher runs them via eligibility.)
150- await db . transaction ( async ( trx ) => {
178+ return db . transaction ( async ( trx ) => {
179+ let rowsChanged = false
151180 for ( const group of groups ) {
152181 const reRunnable = sql `EXISTS (
153182 SELECT 1 FROM ${ tableRowExecutions } re
154183 WHERE re.row_id = ${ userTableRows . id }
155184 AND re.group_id = ${ group . id }
156185 AND re.status IN ('error', 'cancelled')
157186 )`
158- const filters : SQL [ ] = [ eq ( userTableRows . tableId , tableId ) , reRunnable ]
187+ const filters : SQL [ ] = [
188+ eq ( userTableRows . tableId , tableId ) ,
189+ eq ( userTableRows . workspaceId , workspaceId ) ,
190+ reRunnable ,
191+ ]
159192 if ( rowScope ) filters . push ( inArray ( userTableRows . id , rowScope ) )
160193 if ( excluded ) filters . push ( notInArray ( userTableRows . id , excluded ) )
161194
162195 const rowWhere = and ( ...filters ) !
163- await updateTableRowsWithDerivedSecretProvenance ( trx , {
196+ const clearedRows = await updateTableRowsWithDerivedSecretProvenance ( trx , {
164197 rowWhere,
165198 transformation : {
166199 mode : 'remove-columns' ,
@@ -172,11 +205,19 @@ export async function bulkClearWorkflowGroupCells(input: {
172205 eq ( tableRowExecutions . tableId , tableId ) ,
173206 eq ( tableRowExecutions . groupId , group . id ) ,
174207 sql `${ tableRowExecutions . status } IN ('error', 'cancelled')` ,
208+ sql `${ tableRowExecutions . rowId } IN (
209+ SELECT ${ userTableRows . id }
210+ FROM ${ userTableRows }
211+ WHERE ${ userTableRows . tableId } = ${ tableId }
212+ AND ${ userTableRows . workspaceId } = ${ workspaceId }
213+ )` ,
175214 ]
176215 if ( rowScope ) execFilters . push ( inArray ( tableRowExecutions . rowId , rowScope ) )
177216 if ( excluded ) execFilters . push ( notInArray ( tableRowExecutions . rowId , excluded ) )
178- await trx . delete ( tableRowExecutions ) . where ( and ( ...execFilters ) )
217+ const deletedExecutions = await deleteExecutionRows ( trx , execFilters )
218+ rowsChanged ||= clearedRows > 0 || deletedExecutions > 0
179219 }
220+ return rowsChanged
180221 } )
181222}
182223
0 commit comments