From 38e81aec6f0f3949b914e0a0b1d884ceec0f79ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Emre=20Kabakc=C4=B1?= Date: Mon, 18 May 2026 01:03:16 +0100 Subject: [PATCH] fix(device-reconcile): replace uuid[] cast with text[] to avoid PG array parse failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Production app pod logs (image 20260516-235210, 2026-05-17 00:10:06 UTC) repeatedly surfaced: [runs-queue] Run 197444 failed after retries: malformed array literal: "f7623d32-..." [runs-queue] Run 197426 failed after retries: malformed array literal: "f7623d32-..." Smoking gun: `device-reconcile.ts` was the only `::uuid[]` cast site in the entire server codebase (`rg "uuid\[\]"` returns one hit). The pin-self-heal UPDATE bound a `pgTextArray(matchingDeviceIds)` literal as a text parameter and then attempted a `::uuid[]` cast — postgres.js's extended-protocol path doesn't reliably re-parse the bound text as an array before applying the uuid[] cast, so PG sees the raw element bytes and fails with "malformed array literal: ". Fix: drop the uuid[] cast. Cast `device_worker_id::text` and compare against a `::text[]` instead. UUIDs are canonical lowercase, so text-form equality matches the uuid-form 1:1 with no semantic loss. Keeps the multi-device semantics intact (the column stays scalar uuid; this WHERE clause was always genuinely multi-valued). Fixes #781. --- packages/server/src/worker-api/device-reconcile.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/server/src/worker-api/device-reconcile.ts b/packages/server/src/worker-api/device-reconcile.ts index cce919b11..bfd152833 100644 --- a/packages/server/src/worker-api/device-reconcile.ts +++ b/packages/server/src/worker-api/device-reconcile.ts @@ -60,12 +60,19 @@ async function ensureDeviceConnectorWired( // runs even on the fast path so a stale pin doesn't silently strand the feeds. const reconcilePin = async (db: typeof sql, connectionId: number) => { const target = matchingDeviceIds.length === 1 ? matchingDeviceIds[0] : null; + // Compare via text on both sides — passing a `pgTextArray(...)` literal + // through a `::uuid[]` cast trips a postgres "malformed array literal" + // failure under the extended-protocol path postgres.js uses (the bound + // text parameter never gets re-parsed as an array before the uuid[] cast + // runs). `device_worker_id::text = ANY(text[])` sidesteps the cast + // entirely; UUIDs are canonical lowercase so text equality matches the + // uuid form 1:1. await db` UPDATE connections SET device_worker_id = ${target}::uuid, updated_at = NOW() WHERE id = ${connectionId} AND device_worker_id IS DISTINCT FROM ${target}::uuid - AND (device_worker_id IS NULL OR NOT (device_worker_id = ANY(${pgTextArray(matchingDeviceIds)}::uuid[]))) + AND (device_worker_id IS NULL OR NOT (device_worker_id::text = ANY(${pgTextArray(matchingDeviceIds)}::text[]))) `; };