From 99a0a3b6abadbec75535ea4f97ddf8292c6853d8 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Thu, 24 Sep 2026 18:34:05 +0000 Subject: [PATCH] Write the asset rows, and notice when they are not written MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is a unique index on ad_video_assets (creative_id, revision, profile), so re-rendering a revision — exactly what a RENDERER_VERSION bump asks for — violated it on every row. That failure was invisible: the insert's return value was discarded, so the statement failed, the job was still marked ready, and the revision kept the assets of the render it was supposed to replace. Every fix since the version bump has been landing in storage and then being dropped here in silence. The animated banners were rendered, validated, uploaded to the bucket, and then not recorded, which is why the renderer reported "produced 3" while the table showed nothing and the campaign page showed nothing. It is also why the corrected HLS codec strings never reached the existing revisions. The rows are now upserted on that index, for the same reason the objects are overwritten: the design did not change, the renderer did. And the error is checked — a revision whose rows were not written points at nothing, so it fails loudly instead of being marked ready. Co-Authored-By: Claude Opus 5 (1M context) --- worker/video.ts | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/worker/video.ts b/worker/video.ts index b1f3c76..95dd846 100644 --- a/worker/video.ts +++ b/worker/video.ts @@ -111,7 +111,19 @@ export async function processRenderJob( // object is recorded, so a crash between these two steps leaves a complete // set of unpublished assets rather than a creative pointing at media that // is half uploaded. - await supabase.from("ad_video_assets").insert( + // Upsert, not insert, and the error is checked. + // + // There is a unique index on (creative_id, revision, profile), so a + // re-render of the same revision — exactly what a RENDERER_VERSION bump + // asks for — violated it. The result was not an error anybody saw: the + // return value was discarded, so the insert failed, the job was marked + // ready, and the revision kept the assets of the render it was supposed to + // replace. Every fix since the version bump landed in storage and was then + // dropped here in silence. + // + // Replacing the row is right for the same reason replacing the object is: + // the design did not change, the renderer did. + const { error: assetError } = await supabase.from("ad_video_assets").upsert( uploaded.map((u) => ({ creative_id: d.creativeId, owner_id: d.ownerId, @@ -127,7 +139,18 @@ export async function processRenderJob( codecs: u.codecs, published: true, })), + { onConflict: "creative_id,revision,profile" }, ); + if (assetError) { + // Loudly. A revision whose rows were not written is a revision that + // points at nothing, and marking it ready would repeat the failure this + // very change exists to end. + await setJobState(supabase, d.jobRowId, { + state: "failed", + error_code: "asset_rows_failed", + }); + throw new Error(`asset rows failed: ${assetError.message}`); + } const published = await publishRevision(supabase, { creativeId: d.creativeId,