Skip to content

Stale list names in campaign_lists after list is renamed #2734

@ermoi

Description

@ermoi

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

  1. Create a list named "Marketing Team" (ID: 5)
  2. Create a draft campaign and associate it with "Marketing Team"
    • campaign_lists stores: list_id = 5, list_name = "Marketing Team"
  3. Rename the list to "Sales Department"
    • lists table updated: id = 5, name = "Sales Department"
  4. 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_name remains with the old value
  • campaign_lists.list_id correctly 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 prueba

Impact

  • 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.name is updated ✅
  • campaign_lists.list_name remains 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 lists table (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.name in real-time
  • Finished campaigns → snapshot campaign_lists.list_name at send time

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions