Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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'::timestamp),
COALESCE("dateEnd", '1970-01-01T00:00:00Z'::timestamp)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timestamp type mismatch will cause migration to fail

High Severity

The dateStart and dateEnd columns are TIMESTAMP WITH TIME ZONE (per migration V1691658076), but the COALESCE sentinel values use ::timestamp (without time zone). Mixing timestamptz and timestamp in COALESCE triggers an implicit cast that PostgreSQL considers non-immutable (it depends on session timezone). Since expression indexes require all functions to be IMMUTABLE, the CREATE UNIQUE INDEX statement will fail at runtime, blocking the migration entirely. The cast needs to be ::timestamptz instead.

Fix in Cursor Fix in Web

);
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,32 @@ class MemberSegmentAffiliationRepository extends RepositoryBase<

const transaction = this.transaction

const existing = await this.options.database.sequelize.query(
`SELECT "id" FROM "memberSegmentAffiliations"
WHERE "memberId" = :memberId
AND "segmentId" = :segmentId
AND "organizationId" IS NOT DISTINCT FROM :organizationId
AND "dateStart" IS NOT DISTINCT FROM :dateStart
AND "dateEnd" IS NOT DISTINCT FROM :dateEnd
LIMIT 1`,
{
replacements: {
memberId: data.memberId,
segmentId: data.segmentId,
organizationId: data.organizationId,
dateStart: data.dateStart || null,
dateEnd: data.dateEnd || null,
},
type: QueryTypes.SELECT,
transaction,
}
)

if (existing.length > 0) {
await this.updateAffiliation(data.memberId, data.segmentId, data.organizationId)
return this.findById((existing[0] as any).id)
}

const affiliationInsertResult = await this.options.database.sequelize.query(
`INSERT INTO "memberSegmentAffiliations" ("id", "memberId", "segmentId", "organizationId", "dateStart", "dateEnd")
VALUES
Expand Down