From 94feb2ba20994171ea40e17c31c262c90379251e Mon Sep 17 00:00:00 2001 From: mindsers Date: Sun, 23 Aug 2026 22:27:33 +0200 Subject: [PATCH] feat(territories): split the legacy campaign backfill into per-period campaigns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The campaigns migration collapsed all legacy type='campaign' attributions into one synthetic ended 'Campagne' per congregation. This follow-up data migration splits each synthetic into one campaign per date cluster (a gap of more than 30 days between an attribution's start and the latest end seen so far opens a new cluster), named by its period ('Campagne 03/2024'), repoints the attributions and drops the emptied synthetic. Synthetics are recognized by the backfill's exact stamps (name + activatedAt = startDate + endedAt = endDate), so campaigns that went through the real lifecycle — even ones named 'Campagne' — and single-cluster synthetics are untouched. Single data-modifying CTE chain (temp tables don't survive Prisma's statement execution); verified against seeded multi-cluster, decoy and single-cluster states on the dev database. --- .../migration.sql | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 app/database/migrations/20260823200000_split_legacy_campaign_backfill/migration.sql diff --git a/app/database/migrations/20260823200000_split_legacy_campaign_backfill/migration.sql b/app/database/migrations/20260823200000_split_legacy_campaign_backfill/migration.sql new file mode 100644 index 00000000..d8fc10a7 --- /dev/null +++ b/app/database/migrations/20260823200000_split_legacy_campaign_backfill/migration.sql @@ -0,0 +1,95 @@ +-- Follow-up to 20260823000000_add_publishing_campaigns: that backfill collapsed +-- every congregation's legacy `type = 'campaign'` attributions into ONE synthetic +-- ended campaign named 'Campagne'. Real history usually spans several distinct +-- drives, so split each synthetic campaign into one campaign per date cluster: +-- a gap of more than 30 days between an attribution's start and the latest end +-- seen so far opens a new cluster. Synthetic campaigns are recognized by the +-- exact stamps the backfill wrote (name 'Campagne', activatedAt = startDate, +-- endedAt = endDate) — a campaign that went through the real lifecycle never +-- has activation/end timestamps exactly equal to its day-granular bounds. +-- Single-cluster synthetics are left untouched (splitting would be a rename). +-- One data-modifying CTE chain: temp tables don't survive Prisma's statement +-- execution. + +WITH synth AS ( + SELECT id, "congregationId" + FROM "Campaign" + WHERE name = 'Campagne' + AND "activatedAt" = "startDate" + AND "endedAt" = "endDate" +), +attrs AS ( + SELECT a.id, + a."congregationId", + a."campaignId", + a."startDate", + COALESCE(a."endDate", a."startDate") AS eff_end + FROM "Attribution" a + JOIN synth s ON s.id = a."campaignId" +), +marked AS ( + SELECT attrs.*, + CASE + WHEN "startDate" > COALESCE( + MAX(eff_end) OVER ( + PARTITION BY "campaignId" + ORDER BY "startDate", id + ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING + ), + "startDate" + ) + INTERVAL '30 days' + THEN 1 ELSE 0 + END AS opens_cluster + FROM attrs +), +clustered AS ( + SELECT marked.*, + SUM(opens_cluster) OVER (PARTITION BY "campaignId" ORDER BY "startDate", id) AS cluster + FROM marked +), +-- Only campaigns that actually split into several clusters are rewritten. +bounds AS ( + SELECT "campaignId", + "congregationId", + cluster, + MIN("startDate") AS cluster_start, + MAX(eff_end) AS cluster_end + FROM clustered + WHERE "campaignId" IN ( + SELECT "campaignId" FROM clustered GROUP BY "campaignId" HAVING COUNT(DISTINCT cluster) > 1 + ) + GROUP BY "campaignId", "congregationId", cluster +), +-- One already-ended campaign per cluster, named by its period. Cluster bounds +-- within one congregation are disjoint (30-day gaps) and there is only one +-- synthetic campaign per congregation, so (congregationId, startDate, endDate) +-- uniquely identifies the created row for the repointing below. +created AS ( + INSERT INTO "Campaign" ("name", "startDate", "endDate", "activatedAt", "endedAt", "congregationId", "updatedAt") + SELECT 'Campagne ' || to_char(b.cluster_start, 'MM/YYYY'), + b.cluster_start, + b.cluster_end, + b.cluster_start, + b.cluster_end, + b."congregationId", + CURRENT_TIMESTAMP + FROM bounds b + RETURNING id, "congregationId", "startDate", "endDate" +), +repointed AS ( + UPDATE "Attribution" a + SET "campaignId" = c.id + FROM clustered lc + JOIN bounds b + ON b."campaignId" = lc."campaignId" AND b.cluster = lc.cluster + JOIN created c + ON c."congregationId" = b."congregationId" + AND c."startDate" = b.cluster_start + AND c."endDate" = b.cluster_end + WHERE a.id = lc.id + RETURNING lc."campaignId" AS old_campaign_id +) +-- The split synthetics are now empty (every attribution was repointed; the +-- backfill never paused anything) — drop them. +DELETE FROM "Campaign" +WHERE id IN (SELECT DISTINCT old_campaign_id FROM repointed);