From 09935f9b27d27d71e8ab7df2212a9214449367f4 Mon Sep 17 00:00:00 2001 From: "Sanchal (via gbrain)" Date: Sat, 9 May 2026 03:52:05 +0000 Subject: [PATCH] fix: unblock v0.29.1 effective date migration --- src/commands/migrations/v0_29_1.ts | 8 ++- src/core/backfill-effective-date.ts | 80 +++++++++++------------------ 2 files changed, 36 insertions(+), 52 deletions(-) diff --git a/src/commands/migrations/v0_29_1.ts b/src/commands/migrations/v0_29_1.ts index 38b677d6c..f71165d3c 100644 --- a/src/commands/migrations/v0_29_1.ts +++ b/src/commands/migrations/v0_29_1.ts @@ -48,7 +48,9 @@ async function phaseBBackfill(opts: OrchestratorOpts): Promise= opts.maxRows) break; @@ -173,51 +165,39 @@ export async function backfillEffectiveDate( let touched = 0; if (!opts.dryRun) { - // Compute effective_date for each row, then UPDATE in a batch wrapped - // in its own transaction (so SET LOCAL statement_timeout scopes to it). - // postgres.js's `transaction` would be cleaner but we're using executeRaw - // for engine portability; explicit BEGIN/COMMIT does the same on both. - if (isPostgres) { - await engine.executeRaw(`BEGIN`); - await engine.executeRaw(`SET LOCAL statement_timeout = '600s'`); - } - - try { - for (const r of rows) { - const fm = parseFrontmatter(r.frontmatter); - const filename = r.import_filename - || (r.slug.includes('/') ? r.slug.split('/').pop()! : r.slug); - const computed = computeEffectiveDate({ - slug: r.slug, - frontmatter: fm, - filename, - updatedAt: new Date(r.updated_at), - createdAt: new Date(r.created_at), - }); - - // No-op-on-equal: skip the UPDATE if existing matches (saves write - // amplification on re-runs). `force: true` bypasses. - const existingMs = r.effective_date ? new Date(r.effective_date).getTime() : null; - const computedMs = computed.date ? computed.date.getTime() : null; - const datesMatch = existingMs === computedMs; - const sourcesMatch = (r.effective_date_source ?? null) === (computed.source ?? null); + // Compute effective_date for each row, then UPDATE independently. + // This backfill is idempotent and resumable via CHECKPOINT_KEY, so a + // batch-level transaction is unnecessary. More importantly, postgres.js + // rejects raw BEGIN/COMMIT on pooled connections; callers that need a + // transaction must use sql.begin/sql.reserved, which is not exposed by + // the portable BrainEngine interface used here. + for (const r of rows) { + const fm = parseFrontmatter(r.frontmatter); + const filename = r.import_filename + || (r.slug.includes('/') ? r.slug.split('/').pop()! : r.slug); + const computed = computeEffectiveDate({ + slug: r.slug, + frontmatter: fm, + filename, + updatedAt: new Date(r.updated_at), + createdAt: new Date(r.created_at), + }); - if (!opts.force && datesMatch && sourcesMatch) continue; + // No-op-on-equal: skip the UPDATE if existing matches (saves write + // amplification on re-runs). `force: true` bypasses. + const existingMs = r.effective_date ? new Date(r.effective_date).getTime() : null; + const computedMs = computed.date ? computed.date.getTime() : null; + const datesMatch = existingMs === computedMs; + const sourcesMatch = (r.effective_date_source ?? null) === (computed.source ?? null); - await engine.executeRaw( - `UPDATE pages SET effective_date = $1::timestamptz, effective_date_source = $2 WHERE id = $3`, - [computed.date ? computed.date.toISOString() : null, computed.source, r.id], - ); - touched++; - if (computed.source === 'fallback') fallback++; - } + if (!opts.force && datesMatch && sourcesMatch) continue; - if (isPostgres) await engine.executeRaw(`COMMIT`); - } catch (e) { - if (isPostgres) { - try { await engine.executeRaw(`ROLLBACK`); } catch { /* ignore */ } - } - throw e; + await engine.executeRaw( + `UPDATE pages SET effective_date = $1::timestamptz, effective_date_source = $2 WHERE id = $3`, + [computed.date ? computed.date.toISOString() : null, computed.source, r.id], + ); + touched++; + if (computed.source === 'fallback') fallback++; } } else { // Dry run: still count what WOULD change.