-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Description
Bug Description
When a list is renamed, the list_name field in the campaign_lists table is not updated for existing draft campaigns. This results in campaigns showing outdated list names in the UI, even though the list_id correctly references the renamed list.
Steps to Reproduce
- Create a list named "Marketing Team" (ID: 5)
- Create a draft campaign and associate it with "Marketing Team"
campaign_listsstores:list_id = 5,list_name = "Marketing Team"
- Rename the list to "Sales Department"
liststable updated:id = 5,name = "Sales Department"
- Open the draft campaign in the UI
Expected Behavior
The campaign should display the updated list name: "Sales Department"
Actual Behavior
The campaign still shows the old list name: "Marketing Team"
- The UI displays outdated list names
campaign_lists.list_nameremains with the old valuecampaign_lists.list_idcorrectly points to the renamed list- The discrepancy is confusing for users
Database Evidence
-- Show mismatch between stored and current list names
listmonk=# SELECT
listmonk-# c.id,
listmonk-# c.name as campaign,
listmonk-# cl.list_name as stored_name,
listmonk-# l.name as current_name
listmonk-# FROM campaigns c
listmonk-# INNER JOIN campaign_lists cl ON c.id = cl.campaign_id
listmonk-# INNER JOIN lists l ON cl.list_id = l.id
listmonk-# WHERE c.status = 'draft'
listmonk-# AND cl.list_name != l.name;
id | campaign | stored_name | current_name
----+-----------------------------------+-----------------------------+----------------------------
9 | Newsletter semana Septiembre Javi | Lista para envío de pruebas | Lista para envío de pruebaImpact
- User confusion: Campaigns show wrong list names
- Reporting issues: Historical data becomes unclear
- Trust issues: Users may think the system is buggy
- Workflow disruption: Need to verify lists by opening them
Environment
- Listmonk version: v5.1.0 (30846f8 2025-09-09)
- Database: PostgreSQL
- Deployment: Docker
Root Cause
The campaign_lists table stores a denormalized copy of the list name:
list_id(foreign key) ✅list_name(static copy) ❌
When a list is renamed:
lists.nameis updated ✅campaign_lists.list_nameremains unchanged ❌
Proposed Solutions
Option 1: Remove list_name column (Recommended)
Don't store list_name in campaign_lists. Always join with lists table to get current name:
-- Query would become:
SELECT c.*, l.name as list_name
FROM campaigns c
INNER JOIN campaign_lists cl ON c.id = cl.campaign_id
INNER JOIN lists l ON cl.list_id = l.id;Pros:
- No stale data
- Single source of truth
- Automatic updates
Cons:
- Loses historical list names for finished campaigns
Option 2: Update trigger
Create a PostgreSQL trigger to update campaign_lists.list_name when lists.name changes:
CREATE OR REPLACE FUNCTION update_campaign_list_names()
RETURNS TRIGGER AS $$
BEGIN
UPDATE campaign_lists
SET list_name = NEW.name
WHERE list_id = NEW.id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER lists_name_update
AFTER UPDATE OF name ON lists
FOR EACH ROW
WHEN (OLD.name IS DISTINCT FROM NEW.name)
EXECUTE FUNCTION update_campaign_list_names();Pros:
- Automatic synchronization
- Keeps denormalized data
Cons:
- Updates historical campaigns (may not be desired)
- More complex
Option 3: Update only draft campaigns
Trigger that updates only draft campaigns:
CREATE OR REPLACE FUNCTION update_draft_campaign_list_names()
RETURNS TRIGGER AS $$
BEGIN
UPDATE campaign_lists cl
SET list_name = NEW.name
FROM campaigns c
WHERE cl.list_id = NEW.id
AND cl.campaign_id = c.id
AND c.status = 'draft';
RETURN NEW;
END;
$$ LANGUAGE plpgsql;Pros:
- Keeps historical data intact for finished campaigns
- Updates drafts automatically
Cons:
- More complex logic
Option 4: Hybrid approach
- For draft campaigns: Always query from
liststable (Option 1) - For finished campaigns: Keep snapshot in
campaign_lists
This preserves history while showing current names for drafts.
Current Workaround
Manually sync names for draft campaigns:
UPDATE campaign_lists cl
SET list_name = l.name
FROM campaigns c, lists l
WHERE cl.campaign_id = c.id
AND cl.list_id = l.id
AND c.status = 'draft'
AND cl.list_name != l.name;Related Issues
This is similar to the to_send not being recalculated issue, suggesting a broader problem with draft campaigns not being "live" representations of their associated data.
Additional Context
For finished campaigns, keeping the old list name might actually be desirable for historical accuracy (e.g., "This campaign was sent to 'Q3 Prospects' which is now called 'Converted Customers'").
For draft campaigns, showing current data is essential for accurate editing and sending.
Recommendation
Implement Option 4 (Hybrid approach):
- Draft campaigns → query
lists.namein real-time - Finished campaigns → snapshot
campaign_lists.list_nameat send time