Skip to content

Commit ffba34f

Browse files
committed
fix(db): prevent duplicate member segment affiliations
Adds a pre-insert SELECT check in TypeScript and a concrete UNIQUE index migration on memberSegmentAffiliations robustly resolving duplicate affiliation logging problems. Signed-off-by: Prem bharne <prembharne455@gmail.com>
1 parent 44c6b90 commit ffba34f

2 files changed

Lines changed: 50 additions & 0 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
-- 1. Deduplicate existing memberSegmentAffiliations
2+
DELETE FROM "memberSegmentAffiliations" a USING (
3+
SELECT MIN(id) as keep_id, "memberId", "segmentId", "organizationId", "dateStart", "dateEnd"
4+
FROM "memberSegmentAffiliations"
5+
GROUP BY "memberId", "segmentId", "organizationId", "dateStart", "dateEnd"
6+
HAVING COUNT(*) > 1
7+
) b
8+
WHERE a."memberId" = b."memberId"
9+
AND a."segmentId" = b."segmentId"
10+
AND a."organizationId" IS NOT DISTINCT FROM b."organizationId"
11+
AND a."dateStart" IS NOT DISTINCT FROM b."dateStart"
12+
AND a."dateEnd" IS NOT DISTINCT FROM b."dateEnd"
13+
AND a.id <> b.keep_id;
14+
15+
-- 2. Add an index to prevent exact duplicates in the future
16+
-- Using COALESCE ensures that NULL values are logically treated as equal
17+
-- across all supported PostgreSQL versions for the sake of uniqueness.
18+
CREATE UNIQUE INDEX "uq_member_segment_affiliations" ON "memberSegmentAffiliations" (
19+
"memberId",
20+
"segmentId",
21+
COALESCE("organizationId", '00000000-0000-0000-0000-000000000000'::uuid),
22+
COALESCE("dateStart", '1970-01-01T00:00:00Z'::timestamp),
23+
COALESCE("dateEnd", '1970-01-01T00:00:00Z'::timestamp)
24+
);

backend/src/database/repositories/memberSegmentAffiliationRepository.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,32 @@ class MemberSegmentAffiliationRepository extends RepositoryBase<
4444

4545
const transaction = this.transaction
4646

47+
const existing = await this.options.database.sequelize.query(
48+
`SELECT "id" FROM "memberSegmentAffiliations"
49+
WHERE "memberId" = :memberId
50+
AND "segmentId" = :segmentId
51+
AND "organizationId" IS NOT DISTINCT FROM :organizationId
52+
AND "dateStart" IS NOT DISTINCT FROM :dateStart
53+
AND "dateEnd" IS NOT DISTINCT FROM :dateEnd
54+
LIMIT 1`,
55+
{
56+
replacements: {
57+
memberId: data.memberId,
58+
segmentId: data.segmentId,
59+
organizationId: data.organizationId,
60+
dateStart: data.dateStart || null,
61+
dateEnd: data.dateEnd || null,
62+
},
63+
type: QueryTypes.SELECT,
64+
transaction,
65+
}
66+
)
67+
68+
if (existing.length > 0) {
69+
await this.updateAffiliation(data.memberId, data.segmentId, data.organizationId)
70+
return this.findById((existing[0] as any).id)
71+
}
72+
4773
const affiliationInsertResult = await this.options.database.sequelize.query(
4874
`INSERT INTO "memberSegmentAffiliations" ("id", "memberId", "segmentId", "organizationId", "dateStart", "dateEnd")
4975
VALUES

0 commit comments

Comments
 (0)