fix(minions): unify JSONB inserts, tighten nullish coalescing

Three non-blocker cleanups from post-ship review of v0.11.0:

- queue.ts add() and completeJob(): pre-stringifying with JSON.stringify
  while other sites pass raw objects with $n::jsonb casts. postgres.js
  double-encodes if you stringify first — works on PGLite (text→JSONB
  auto-cast), fails silently on real PG. Unify on raw object + explicit
  $n::jsonb cast.
- queue.ts readChildCompletions: since clause used sent_at > $2 relying
  on PG's implicit text→TIMESTAMPTZ coercion. Explicit $2::timestamptz
  is safer and clearer.
- types.ts rowToMinionJob: parent_job_id used || which coerces 0 to null.
  Harmless today (SERIAL IDs start at 1) but ?? is semantically correct.

All 110 unit tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-18 06:47:53 +08:00
co-authored by Claude Opus 4.7
parent 5382ee0309
commit c502b7ea6d
2 changed files with 7 additions and 7 deletions
+6 -6
View File
@@ -113,13 +113,13 @@ export class MinionQueue {
? `INSERT INTO minion_jobs (name, queue, status, priority, data, max_attempts, backoff_type, ? `INSERT INTO minion_jobs (name, queue, status, priority, data, max_attempts, backoff_type,
backoff_delay, backoff_jitter, delay_until, parent_job_id, on_child_fail, backoff_delay, backoff_jitter, delay_until, parent_job_id, on_child_fail,
depth, max_children, timeout_ms, remove_on_complete, remove_on_fail, idempotency_key) depth, max_children, timeout_ms, remove_on_complete, remove_on_fail, idempotency_key)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
ON CONFLICT (idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING ON CONFLICT (idempotency_key) WHERE idempotency_key IS NOT NULL DO NOTHING
RETURNING *` RETURNING *`
: `INSERT INTO minion_jobs (name, queue, status, priority, data, max_attempts, backoff_type, : `INSERT INTO minion_jobs (name, queue, status, priority, data, max_attempts, backoff_type,
backoff_delay, backoff_jitter, delay_until, parent_job_id, on_child_fail, backoff_delay, backoff_jitter, delay_until, parent_job_id, on_child_fail,
depth, max_children, timeout_ms, remove_on_complete, remove_on_fail, idempotency_key) depth, max_children, timeout_ms, remove_on_complete, remove_on_fail, idempotency_key)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18) VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
RETURNING *`; RETURNING *`;
const params = [ const params = [
@@ -127,7 +127,7 @@ export class MinionQueue {
opts?.queue ?? 'default', opts?.queue ?? 'default',
childStatus, childStatus,
opts?.priority ?? 0, opts?.priority ?? 0,
JSON.stringify(data ?? {}), data ?? {},
opts?.max_attempts ?? 3, opts?.max_attempts ?? 3,
opts?.backoff_type ?? 'exponential', opts?.backoff_type ?? 'exponential',
opts?.backoff_delay ?? 1000, opts?.backoff_delay ?? 1000,
@@ -450,11 +450,11 @@ export class MinionQueue {
} }
const rows = await tx.executeRaw<Record<string, unknown>>( const rows = await tx.executeRaw<Record<string, unknown>>(
`UPDATE minion_jobs SET status = 'completed', result = $1, `UPDATE minion_jobs SET status = 'completed', result = $1::jsonb,
finished_at = now(), lock_token = NULL, lock_until = NULL, updated_at = now() finished_at = now(), lock_token = NULL, lock_until = NULL, updated_at = now()
WHERE id = $2 AND status = 'active' AND lock_token = $3 WHERE id = $2 AND status = 'active' AND lock_token = $3
RETURNING *`, RETURNING *`,
[result ? JSON.stringify(result) : null, id, lockToken] [result ?? null, id, lockToken]
); );
if (rows.length === 0) return null; if (rows.length === 0) return null;
@@ -841,7 +841,7 @@ export class MinionQueue {
const params: unknown[] = [parentId]; const params: unknown[] = [parentId];
let sinceClause = ''; let sinceClause = '';
if (opts?.since) { if (opts?.since) {
sinceClause = ` AND sent_at > $2`; sinceClause = ` AND sent_at > $2::timestamptz`;
params.push(opts.since.toISOString()); params.push(opts.since.toISOString());
} }
+1 -1
View File
@@ -284,7 +284,7 @@ export function rowToMinionJob(row: Record<string, unknown>): MinionJob {
lock_token: (row.lock_token as string) || null, lock_token: (row.lock_token as string) || null,
lock_until: row.lock_until ? new Date(row.lock_until as string) : null, lock_until: row.lock_until ? new Date(row.lock_until as string) : null,
delay_until: row.delay_until ? new Date(row.delay_until as string) : null, delay_until: row.delay_until ? new Date(row.delay_until as string) : null,
parent_job_id: (row.parent_job_id as number) || null, parent_job_id: (row.parent_job_id as number | null) ?? null,
on_child_fail: row.on_child_fail as ChildFailPolicy, on_child_fail: row.on_child_fail as ChildFailPolicy,
tokens_input: (row.tokens_input as number) ?? 0, tokens_input: (row.tokens_input as number) ?? 0,
tokens_output: (row.tokens_output as number) ?? 0, tokens_output: (row.tokens_output as number) ?? 0,