Skip to content

Commit 82e654d

Browse files
authored
fix(cron): bind stale async-job cutoffs through the column encoder (#6327)
The stale-processing predicate interpolated `Date` values straight into a raw `sql` template. A raw template carries no column context, so drizzle skips `PgTimestamp.mapToDriverValue` (which stringifies via `toISOString`) and postgres-js receives a `Date` it cannot serialize under the pools' `prepare: false` / `fetch_types: false` options. Every run of the job has failed its async-job sweep since the change shipped, leaving stuck jobs unreaped while the surrounding typed `lt(column, date)` sweeps succeeded. Bind both cutoffs with `sql.param(date, column)`, matching the workflow sweep in the same handler. The testing `sql` mock already rejected `sql.param(date)` for this reason but not the interpolated form that shipped, so extend it to cover both. That guard alone fails three existing tests when the fix is reverted, and the full suite shows no other route binding a bare Date this way.
1 parent 71d7d8d commit 82e654d

2 files changed

Lines changed: 16 additions & 2 deletions

File tree

apps/sim/app/api/cron/cleanup-stale-executions/route.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,10 +245,14 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
245245
= (${asyncJobs.metadata}->>'maxDurationSeconds')::numeric
246246
ELSE FALSE
247247
END`
248+
// A bare `Date` in a raw template reaches the driver unserialized; `sql.param`
249+
// binds it through the column encoder, as `lt(column, date)` does elsewhere here.
250+
const staleProcessingCutoff = sql.param(now, asyncJobs.startedAt)
251+
const staleProcessingFallbackCutoff = sql.param(staleThreshold, asyncJobs.startedAt)
248252
const staleProcessingDurationPredicate = sql<boolean>`CASE
249253
WHEN ${hasPositiveMaxDuration}
250-
THEN ${asyncJobs.startedAt} + ((${asyncJobs.metadata}->>'maxDurationSeconds')::double precision * interval '1 second') < ${now}
251-
ELSE ${asyncJobs.startedAt} < ${staleThreshold}
254+
THEN ${asyncJobs.startedAt} + ((${asyncJobs.metadata}->>'maxDurationSeconds')::double precision * interval '1 second') < ${staleProcessingCutoff}
255+
ELSE ${asyncJobs.startedAt} < ${staleProcessingFallbackCutoff}
252256
END`
253257
const staleProcessingPredicate = and(
254258
eq(asyncJobs.status, JOB_STATUS.PROCESSING),

packages/testing/src/mocks/database.mock.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ import { vi } from 'vitest'
66
*/
77
export function createMockSql() {
88
const sqlFn = (strings: TemplateStringsArray, ...values: any[]) => {
9+
// Same hazard as `sql.param(date)` below, and the form that actually shipped:
10+
// an interpolated `Date` carries no column context, so drizzle skips
11+
// `PgTimestamp.mapToDriverValue` and postgres-js receives a Date it cannot serialize.
12+
if (values.some((value) => value instanceof Date)) {
13+
throw new Error(
14+
'sql`…${date}` interpolates a Date without an encoder, which reaches ' +
15+
'postgres-js as a Date object its unsafe path cannot serialize. Bind ' +
16+
'through the matching column: sql.param(date, table.timestampColumn).'
17+
)
18+
}
919
const fragment = {
1020
strings,
1121
values,

0 commit comments

Comments
 (0)