Skip to content

Commit 29cade9

Browse files
Simplify stripping column names
1 parent 38f5130 commit 29cade9

4 files changed

Lines changed: 105 additions & 123 deletions

File tree

apps/sim/hooks/queries/tables.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,16 @@ function hasRunningGroupExecution(rows: TableRow[] | undefined): boolean {
8484
return false
8585
}
8686

87+
function hasRunningGroupExecutionInPages(
88+
pages: TableRowsResponse[] | undefined
89+
): boolean {
90+
if (!pages) return false
91+
for (const page of pages) {
92+
if (hasRunningGroupExecution(page.rows)) return true
93+
}
94+
return false
95+
}
96+
8797
const logger = createLogger('TableQueries')
8898

8999
type TableQueryScope = 'active' | 'archived' | 'all'
@@ -317,8 +327,9 @@ export function useInfiniteTableRows({
317327
*/
318328
refetchInterval: (query) => {
319329
if (queryClient.isMutating() > 0) return false
320-
const rows = query.state.data?.pages.flatMap((p) => p.rows)
321-
return hasRunningGroupExecution(rows) ? ROWS_POLL_INTERVAL_WHILE_RUNNING_MS : false
330+
return hasRunningGroupExecutionInPages(query.state.data?.pages)
331+
? ROWS_POLL_INTERVAL_WHILE_RUNNING_MS
332+
: false
322333
},
323334
refetchIntervalInBackground: false,
324335
})

apps/sim/lib/table/deps.ts

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,24 +95,25 @@ export function optimisticallyScheduleNewlyEligibleGroups(
9595
}
9696

9797
let next: RowExecutions | null = null
98+
let flipped = 0
99+
let skipped = 0
98100
for (const group of groups) {
99-
const tag = `[OptimisticCascade] row=${beforeRow.id} group=${group.id}`
100101
if (group.autoRun === false) {
101-
logger.debug(`${tag} → skip: autoRun=false`)
102+
skipped++
102103
continue
103104
}
104105
if (!areGroupDepsSatisfied(group, afterRow)) {
105-
logger.debug(`${tag} → skip: deps unmet on afterRow`)
106+
skipped++
106107
continue
107108
}
108109

109110
const exec = beforeRow.executions?.[group.id]
110111
if (exec?.status === 'queued' || exec?.status === 'running') {
111-
logger.debug(`${tag} → skip: already ${exec.status}`)
112+
skipped++
112113
continue
113114
}
114115
if (exec?.status === 'pending' && exec.jobId) {
115-
logger.debug(`${tag} → skip: pending+jobId (worker reserved)`)
116+
skipped++
116117
continue
117118
}
118119

@@ -121,13 +122,11 @@ export function optimisticallyScheduleNewlyEligibleGroups(
121122
const becameSatisfied = !wasSatisfied
122123
const isRetryable = exec?.status === 'cancelled' || exec?.status === 'error'
123124
if (!becameSatisfied && !isStaleCompleted && !isRetryable && exec) {
124-
logger.debug(`${tag} → skip: no fire reason (status=${exec?.status})`)
125+
skipped++
125126
continue
126127
}
127128

128-
logger.debug(
129-
`${tag} → flip to pending (becameSatisfied=${becameSatisfied} stale=${isStaleCompleted} retry=${isRetryable})`
130-
)
129+
flipped++
131130
if (next === null) next = { ...(beforeRow.executions ?? {}) }
132131
const pending: RowExecutionMetadata = {
133132
status: 'pending',
@@ -138,5 +137,10 @@ export function optimisticallyScheduleNewlyEligibleGroups(
138137
}
139138
next[group.id] = pending
140139
}
140+
if (flipped > 0) {
141+
logger.debug(
142+
`[OptimisticCascade] row=${beforeRow.id} flipped=${flipped} skipped=${skipped}`
143+
)
144+
}
141145
return next
142146
}

apps/sim/lib/table/service.ts

Lines changed: 10 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,12 @@ import {
6161
validateTableName,
6262
validateTableSchema,
6363
} from './validation'
64-
import { assertValidSchema, scheduleRunsForRows, scheduleRunsForTable } from './workflow-columns'
64+
import {
65+
assertValidSchema,
66+
scheduleRunsForRows,
67+
scheduleRunsForTable,
68+
stripGroupDeps,
69+
} from './workflow-columns'
6570

6671
const logger = createLogger('TableService')
6772

@@ -2400,16 +2405,7 @@ export async function deleteColumn(
24002405
}
24012406
next = { ...next, outputs: remaining }
24022407
}
2403-
const filtered = next.dependencies?.columns?.filter((d) => d !== actualName)
2404-
if (filtered && filtered.length !== (next.dependencies?.columns?.length ?? 0)) {
2405-
next = {
2406-
...next,
2407-
...(filtered.length > 0
2408-
? { dependencies: { columns: filtered } }
2409-
: { dependencies: undefined }),
2410-
}
2411-
}
2412-
return next
2408+
return stripGroupDeps(next, new Set([actualName]))
24132409
})
24142410
.filter((g) => g.id !== groupRemovedId)
24152411

@@ -2502,17 +2498,7 @@ export async function deleteColumns(
25022498
})
25032499
updatedGroups = updatedGroups
25042500
.filter((g) => !removedGroupIds.has(g.id))
2505-
.map((group) => {
2506-
const depCols = group.dependencies?.columns?.filter((d) => !namesToDelete.has(d))
2507-
const colsChanged = depCols && depCols.length !== (group.dependencies?.columns?.length ?? 0)
2508-
if (!colsChanged) return group
2509-
return {
2510-
...group,
2511-
...(depCols && depCols.length > 0
2512-
? { dependencies: { columns: depCols } }
2513-
: { dependencies: undefined }),
2514-
}
2515-
})
2501+
.map((group) => stripGroupDeps(group, namesToDelete))
25162502
const updatedSchema: TableSchema = {
25172503
...schema,
25182504
columns: remaining,
@@ -2983,17 +2969,7 @@ export async function updateWorkflowGroup(
29832969
// refs so we don't leave dangling-column deps that fail schema validation.
29842970
const nextGroups = groups
29852971
.map((g, i) => (i === groupIndex ? updatedGroup : g))
2986-
.map((g) => {
2987-
if (g.id === updatedGroup.id) return g
2988-
const filtered = g.dependencies?.columns?.filter((d) => !removedColumnNames.has(d))
2989-
if (!filtered || filtered.length === (g.dependencies?.columns?.length ?? 0)) return g
2990-
return {
2991-
...g,
2992-
...(filtered.length > 0
2993-
? { dependencies: { columns: filtered } }
2994-
: { dependencies: undefined }),
2995-
}
2996-
})
2972+
.map((g) => (g.id === updatedGroup.id ? g : stripGroupDeps(g, removedColumnNames)))
29972973
const updatedSchema: TableSchema = {
29982974
...schema,
29992975
columns: nextColumns,
@@ -3424,16 +3400,7 @@ export async function deleteWorkflowGroup(
34243400
// Strip those refs so we don't leave dangling-column deps behind.
34253401
const nextGroups = groups
34263402
.filter((g) => g.id !== data.groupId)
3427-
.map((g) => {
3428-
const filtered = g.dependencies?.columns?.filter((d) => !removedColumnNames.has(d))
3429-
if (!filtered || filtered.length === (g.dependencies?.columns?.length ?? 0)) return g
3430-
return {
3431-
...g,
3432-
...(filtered.length > 0
3433-
? { dependencies: { columns: filtered } }
3434-
: { dependencies: undefined }),
3435-
}
3436-
})
3403+
.map((g) => stripGroupDeps(g, removedColumnNames))
34373404
const updatedSchema: TableSchema = {
34383405
...schema,
34393406
columns: schema.columns.filter((c) => !removedColumnNames.has(c.name)),

apps/sim/lib/table/workflow-columns.ts

Lines changed: 69 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -47,52 +47,52 @@ export {
4747
* cells win over the exec metadata, so deleting an output value re-arms the
4848
* row for the cascade and for manual incomplete-mode runs.
4949
*/
50-
export function isGroupEligible(
50+
/**
51+
* Reason codes the eligibility predicate emits. Stable strings so the caller
52+
* can aggregate skip reasons into one summary log per scheduler call instead
53+
* of allocating a per-cell debug line.
54+
*/
55+
export type EligibilityReason =
56+
| 'eligible'
57+
| 'autoRun-off'
58+
| 'in-flight'
59+
| 'completed-on-auto'
60+
| 'error-on-auto'
61+
| 'completed-on-incomplete'
62+
| 'manual-bypass'
63+
| 'deps-unmet'
64+
65+
export function classifyEligibility(
5166
group: WorkflowGroup,
5267
row: TableRow,
5368
opts?: { isManualRun?: boolean; mode?: 'all' | 'incomplete' }
54-
): boolean {
69+
): EligibilityReason {
5570
const isManualRun = opts?.isManualRun ?? false
5671
const mode = opts?.mode ?? 'all'
57-
const tag = `[Eligibility] row=${row.id} group=${group.id} manual=${isManualRun} mode=${mode}`
5872

59-
if (group.autoRun === false && !isManualRun) {
60-
logger.debug(`${tag} → skip: autoRun=false on auto-fire`)
61-
return false
62-
}
73+
if (group.autoRun === false && !isManualRun) return 'autoRun-off'
6374

6475
const exec = row.executions?.[group.id]
65-
if (isExecInFlight(exec)) {
66-
logger.debug(`${tag} → skip: in-flight (status=${exec?.status})`)
67-
return false
68-
}
76+
if (isExecInFlight(exec)) return 'in-flight'
6977
const status = exec?.status
7078

7179
const completedAndFilled = status === 'completed' && areOutputsFilled(group, row)
72-
if (!isManualRun && completedAndFilled) {
73-
logger.debug(`${tag} → skip: completed+filled on auto-fire`)
74-
return false
75-
}
80+
if (!isManualRun && completedAndFilled) return 'completed-on-auto'
7681
// Auto-fire skips `error` to avoid infinite-retry loops on a deterministic
77-
// failure. `cancelled` doesn't get the same treatment — cancellation is
78-
// user-initiated, and once an upstream dep re-fires the cascade, the user
79-
// almost certainly wants the chain to continue.
80-
if (!isManualRun && status === 'error') {
81-
logger.debug(`${tag} → skip: terminal status=error on auto-fire`)
82-
return false
83-
}
84-
if (mode === 'incomplete' && completedAndFilled) {
85-
logger.debug(`${tag} → skip: completed+filled on mode=incomplete`)
86-
return false
87-
}
82+
// failure. `cancelled` is left runnable — cancellation is user-initiated.
83+
if (!isManualRun && status === 'error') return 'error-on-auto'
84+
if (mode === 'incomplete' && completedAndFilled) return 'completed-on-incomplete'
8885

89-
if (isManualRun && group.autoRun === false) {
90-
logger.debug(`${tag} → eligible: manual on autoRun=false group (deps bypassed)`)
91-
return true
92-
}
93-
const depsOk = areGroupDepsSatisfied(group, row)
94-
logger.debug(`${tag}${depsOk ? 'eligible: deps satisfied' : 'skip: deps unmet'}`)
95-
return depsOk
86+
if (isManualRun && group.autoRun === false) return 'manual-bypass'
87+
return areGroupDepsSatisfied(group, row) ? 'eligible' : 'deps-unmet'
88+
}
89+
90+
export function isGroupEligible(
91+
group: WorkflowGroup,
92+
row: TableRow,
93+
opts?: { isManualRun?: boolean; mode?: 'all' | 'incomplete' }
94+
): boolean {
95+
return classifyEligibility(group, row, opts) === 'eligible'
9696
}
9797

9898
/**
@@ -130,18 +130,19 @@ export async function scheduleRunsForRows(
130130
const groups = groupIdFilter ? allGroups.filter((g) => groupIdFilter.has(g.id)) : allGroups
131131
if (groups.length === 0) return { triggered: 0 }
132132

133-
logger.debug(
134-
`[Cascade] scheduleRunsForRows table=${table.id} rows=${rows.length} groups=${groups.length} manual=${opts?.isManualRun ?? false} mode=${opts?.mode ?? 'all'} scoped=${groupIdFilter ? 'yes' : 'no'}`
135-
)
136-
137133
const orderedRows = rows.length <= 1 ? rows : [...rows].sort((a, b) => a.position - b.position)
138134

139135
const pendingRuns: RunGroupCellOptions[] = []
136+
const reasonCounts: Partial<Record<EligibilityReason, number>> = {}
140137

141138
for (const row of orderedRows) {
142139
for (const group of groups) {
143-
if (!isGroupEligible(group, row, { isManualRun: opts?.isManualRun, mode: opts?.mode }))
144-
continue
140+
const reason = classifyEligibility(group, row, {
141+
isManualRun: opts?.isManualRun,
142+
mode: opts?.mode,
143+
})
144+
reasonCounts[reason] = (reasonCounts[reason] ?? 0) + 1
145+
if (reason !== 'eligible') continue
145146
pendingRuns.push({
146147
tableId: table.id,
147148
tableName: table.name,
@@ -154,6 +155,10 @@ export async function scheduleRunsForRows(
154155
}
155156
}
156157

158+
logger.debug(
159+
`[Cascade] table=${table.id} rows=${rows.length} groups=${groups.length} manual=${opts?.isManualRun ?? false} mode=${opts?.mode ?? 'all'} reasons=${JSON.stringify(reasonCounts)}`
160+
)
161+
157162
if (pendingRuns.length === 0) return { triggered: 0 }
158163

159164
logger.info(`Scheduling ${pendingRuns.length} workflow group cell run(s) for table=${table.id}`)
@@ -418,14 +423,13 @@ export async function cancelWorkflowGroupRuns(tableId: string, rowId?: string):
418423
}
419424

420425
/**
421-
* Generalized run helper. Three public wrappers below pin specific
422-
* argument shapes; this is the shared inner primitive.
423-
*
424-
* `groupIds` omitted = every workflow group on the table.
425-
* `rowIds` omitted = every row (with `mode === 'incomplete'` filter applied
426-
* per group when targeting specific groups).
426+
* Run a set of groups across the table or a row subset. Single canonical
427+
* user-driven run op — every UI gesture (single cell, per-row Play, action-bar
428+
* Play/Refresh, column-header menu) reduces to this. `mode: 'all'` re-runs
429+
* completed cells; `mode: 'incomplete'` skips them. `groupIds` omitted = every
430+
* workflow group on the table. `rowIds` omitted = every row.
427431
*/
428-
async function runWorkflowGroupsInternal(opts: {
432+
export async function runWorkflowColumn(opts: {
429433
tableId: string
430434
workspaceId: string
431435
mode: 'all' | 'incomplete'
@@ -444,7 +448,7 @@ async function runWorkflowGroupsInternal(opts: {
444448
if (targetGroups.length === 0) return { triggered: 0 }
445449

446450
logger.info(
447-
`[Cascade] [${requestId}] runWorkflowColumn table=${tableId} groups=[${targetGroups.map((g) => g.id).join(',')}] rows=${rowIds ? `[${rowIds.join(',')}]` : 'all'} mode=${mode}`
451+
`[Cascade] [${requestId}] manual run table=${tableId} groups=[${targetGroups.map((g) => g.id).join(',')}] rows=${rowIds ? `[${rowIds.join(',')}]` : 'all'} mode=${mode}`
448452
)
449453

450454
const filters = [eq(userTableRows.tableId, tableId), eq(userTableRows.workspaceId, workspaceId)]
@@ -520,32 +524,28 @@ async function runWorkflowGroupsInternal(opts: {
520524
})
521525
}
522526

527+
// ───────────────────────────── Validation ─────────────────────────────
528+
523529
/**
524-
* Run a set of groups across the table or a row subset. Single canonical
525-
* user-driven run op — every UI gesture (single cell, per-row Play, action-bar
526-
* Play/Refresh, column-header menu) reduces to this. `mode: 'all'` re-runs
527-
* completed cells; `mode: 'incomplete'` skips them.
530+
/**
531+
* Removes the given column names from a group's `dependencies.columns`. When
532+
* the resulting list is empty, drops the `dependencies` field entirely so
533+
* schema validation doesn't see an empty-deps object. Returns the same group
534+
* reference when nothing changed.
528535
*/
529-
export async function runWorkflowColumn(opts: {
530-
tableId: string
531-
workspaceId: string
532-
groupIds: string[]
533-
mode: 'all' | 'incomplete'
534-
rowIds?: string[]
535-
requestId: string
536-
}): Promise<{ triggered: number }> {
537-
return runWorkflowGroupsInternal({
538-
tableId: opts.tableId,
539-
workspaceId: opts.workspaceId,
540-
groupIds: opts.groupIds,
541-
rowIds: opts.rowIds,
542-
mode: opts.mode,
543-
requestId: opts.requestId,
544-
})
536+
export function stripGroupDeps(group: WorkflowGroup, removed: ReadonlySet<string>): WorkflowGroup {
537+
const cols = group.dependencies?.columns
538+
if (!cols || cols.length === 0) return group
539+
const filtered = cols.filter((d) => !removed.has(d))
540+
if (filtered.length === cols.length) return group
541+
return {
542+
...group,
543+
...(filtered.length > 0
544+
? { dependencies: { columns: filtered } }
545+
: { dependencies: undefined }),
546+
}
545547
}
546548

547-
// ───────────────────────────── Validation ─────────────────────────────
548-
549549
/**
550550
* Validates schema-level invariants. Run on every `addTableColumn`,
551551
* `addWorkflowGroup`, `updateWorkflowGroup`, `renameColumn`, `reorderColumns`,

0 commit comments

Comments
 (0)