Skip to content

fix: last-message JOIN uses rowid subquery, not timestamp equality (closes #282) - #283

Open
HalemoGPA wants to merge 1 commit into
lharries:mainfrom
HalemoGPA:fix/282-last-message-join
Open

fix: last-message JOIN uses rowid subquery, not timestamp equality (closes #282)#283
HalemoGPA wants to merge 1 commit into
lharries:mainfrom
HalemoGPA:fix/282-last-message-join

Conversation

@HalemoGPA

Copy link
Copy Markdown

Problem

Per #282, list_chats, get_chat, and get_direct_chat_by_contact all use:

LEFT JOIN messages m ON c.jid = m.chat_jid
                    AND c.last_message_time = m.timestamp

which has two failure modes shown below.

Reproduction

-- 1) Drift: last_message_time and messages.timestamp disagree by one microsecond
INSERT INTO chats    VALUES ('c2@s.whatsapp.net','DriftChat','2026-06-25T10:00:00.100000');
INSERT INTO messages VALUES ('m2','c2@s.whatsapp.net','...','present-me!','2026-06-25T10:00:00.100001',...);

-- 2) Duplication: two messages sharing timestamp
INSERT INTO chats    VALUES ('c1@s.whatsapp.net','DupChat','2026-06-25T10:00:00');
INSERT INTO messages VALUES ('m1a','c1@s.whatsapp.net','...','first' ,'2026-06-25T10:00:00',...);
INSERT INTO messages VALUES ('m1b','c1@s.whatsapp.net','...','second','2026-06-25T10:00:00',...);

list_chats today:

c2@s.whatsapp.net  DriftChat  None            <-- BUG: last_message dropped
c1@s.whatsapp.net  DupChat    'first'
c1@s.whatsapp.net  DupChat    'second'         <-- BUG: chat row duplicated

Fix

Switch all three functions to a rowid subquery:

LEFT JOIN messages m ON m.rowid = (
    SELECT rowid FROM messages
    WHERE chat_jid = c.jid
    ORDER BY timestamp DESC
    LIMIT 1
)
  • Always joins to the actual latest message regardless of last_message_time accuracy.
  • Deterministic on ties (LIMIT 1 picks exactly one row).

After the fix, list_chats returns:

c2@s.whatsapp.net  DriftChat  'present-me!'    <-- correct
c1@s.whatsapp.net  DupChat    'first'          <-- one row per chat

Diff

  • whatsapp-mcp-server/whatsapp.py: 1 file, +26 / -7
  • No new dependencies
  • No schema changes
  • No bridge changes

Verified

Fixture asserts all three fixed:

  • list_chats(limit=50) — DriftChat returns actual last message, DupChat single row.
  • get_chat('c2@s.whatsapp.net').last_message == 'present-me!' (was None).
  • get_direct_chat_by_contact('c2').last_message == 'present-me!' (was None).

Perf note

Neutral to slightly better with idx_messages_chat_time (from #278/PR #279 if merged) — the subquery becomes a covering index probe. The motivation here is correctness, not perf; #278 covers the perf story.

What this closes

Closes #282.

list_chats, get_chat and get_direct_chat_by_contact all build the "last
message per chat" side of their result with

    LEFT JOIN messages m ON c.jid = m.chat_jid
                        AND c.last_message_time = m.timestamp

which has two failure modes:

1. Drift: if c.last_message_time and messages.timestamp disagree by even
   one microsecond (or one is a truncation of the other) the JOIN falls
   through to NULL and the chat's last_message / last_sender /
   last_is_from_me come back empty although messages exist.

2. Duplication: two messages in the same chat sharing an exact timestamp
   (rapid consecutive sends, forwarded bursts) each match, so list_chats
   returns one chat row per matching message.

Switching all three functions to a rowid subquery -

    LEFT JOIN messages m ON m.rowid = (
        SELECT rowid FROM messages
        WHERE chat_jid = c.jid
        ORDER BY timestamp DESC
        LIMIT 1
    )

- always joins to the actual latest message regardless of
  last_message_time accuracy, and
- picks exactly one row on ties (deterministic LIMIT 1).

Closes lharries#282.

Reproduction fixture asserts:
- DriftChat (last_message_time one microsecond ahead of the actual
  message.timestamp) now returns 'present-me!' instead of None.
- DupChat (two messages sharing '2026-06-25T10:00:00') returns exactly
  one chat row.
- FineChat (aligned timestamps) still works.
- get_chat and get_direct_chat_by_contact all three FIXED behaviors.

Perf: with idx_messages_chat_time in place the subquery is a covering
index probe, so this change is neutral to slightly better on latency.
The motivation here is correctness; lharries#278 handles the perf story.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

correctness+perf: last-message JOIN uses timestamp equality; can duplicate or drop chat rows

2 participants