diff --git a/backend/src/database/migrations/V1775000000__fix-member-segment-affiliations.sql b/backend/src/database/migrations/V1775000000__fix-member-segment-affiliations.sql new file mode 100644 index 0000000000..009a98dc69 --- /dev/null +++ b/backend/src/database/migrations/V1775000000__fix-member-segment-affiliations.sql @@ -0,0 +1,24 @@ +-- 1. Deduplicate existing memberSegmentAffiliations +DELETE FROM "memberSegmentAffiliations" a USING ( + SELECT MIN(id) as keep_id, "memberId", "segmentId", "organizationId", "dateStart", "dateEnd" + FROM "memberSegmentAffiliations" + GROUP BY "memberId", "segmentId", "organizationId", "dateStart", "dateEnd" + HAVING COUNT(*) > 1 +) b +WHERE a."memberId" = b."memberId" +AND a."segmentId" = b."segmentId" +AND a."organizationId" IS NOT DISTINCT FROM b."organizationId" +AND a."dateStart" IS NOT DISTINCT FROM b."dateStart" +AND a."dateEnd" IS NOT DISTINCT FROM b."dateEnd" +AND a.id <> b.keep_id; + +-- 2. Add an index to prevent exact duplicates in the future +-- Using COALESCE ensures that NULL values are logically treated as equal +-- across all supported PostgreSQL versions for the sake of uniqueness. +CREATE UNIQUE INDEX "uq_member_segment_affiliations" ON "memberSegmentAffiliations" ( + "memberId", + "segmentId", + COALESCE("organizationId", '00000000-0000-0000-0000-000000000000'::uuid), + COALESCE("dateStart", '1970-01-01T00:00:00Z'::timestamptz), + COALESCE("dateEnd", '1970-01-01T00:00:00Z'::timestamptz) +); diff --git a/backend/src/database/repositories/memberSegmentAffiliationRepository.ts b/backend/src/database/repositories/memberSegmentAffiliationRepository.ts index b55bf6d5ec..9f516c4a68 100644 --- a/backend/src/database/repositories/memberSegmentAffiliationRepository.ts +++ b/backend/src/database/repositories/memberSegmentAffiliationRepository.ts @@ -48,6 +48,14 @@ class MemberSegmentAffiliationRepository extends RepositoryBase< `INSERT INTO "memberSegmentAffiliations" ("id", "memberId", "segmentId", "organizationId", "dateStart", "dateEnd") VALUES (:id, :memberId, :segmentId, :organizationId, :dateStart, :dateEnd) + ON CONFLICT ( + "memberId", + "segmentId", + COALESCE("organizationId", '00000000-0000-0000-0000-000000000000'::uuid), + COALESCE("dateStart", '1970-01-01T00:00:00Z'::timestamptz), + COALESCE("dateEnd", '1970-01-01T00:00:00Z'::timestamptz) + ) + DO UPDATE SET "organizationId" = EXCLUDED."organizationId" RETURNING "id" `, {