| 127 | * claimed the slot; the caller returns 409 when it didn't. |
| 128 | */ |
| 129 | export async function markTableJobRunning( |
| 130 | tableId: string, |
| 131 | jobId: string, |
| 132 | type: TableJobType, |
| 133 | /** Type-specific scope persisted to `table_jobs.payload` (e.g. {@link TableDeleteJobPayload}) |
| 134 | * so read paths can mask the job's effect while it runs. */ |
| 135 | payload?: unknown |
| 136 | ): Promise<boolean> { |
| 137 | // workspace_id is immutable; the atomic gate is the INSERT's conflict, not this read. |
| 138 | const [def] = await db |
| 139 | .select({ workspaceId: userTableDefinitions.workspaceId }) |
| 140 | .from(userTableDefinitions) |
| 141 | .where(eq(userTableDefinitions.id, tableId)) |
| 142 | .limit(1) |
| 143 | if (!def) return false |
| 144 | const inserted = await db |
| 145 | .insert(tableJobs) |
| 146 | .values({ |
| 147 | id: jobId, |
| 148 | tableId, |
| 149 | workspaceId: def.workspaceId, |
| 150 | type, |
| 151 | status: 'running', |
| 152 | payload: payload ?? null, |
| 153 | }) |
| 154 | .onConflictDoNothing() |
| 155 | .returning({ id: tableJobs.id }) |
| 156 | return inserted.length > 0 |
| 157 | } |
| 158 | |
| 159 | /** |
| 160 | * Releases a claim taken by {@link markTableJobRunning} for a synchronous job — deletes the |