Page MenuHomePhabricator

[keyserver] Introduce columns in messages table to support pinned messages
ClosedPublic

Authored by rohan on Mar 1 2023, 7:28 AM.
Tags
None
Referenced Files
Unknown Object (File)
Thu, Apr 4, 7:00 AM
Unknown Object (File)
Fri, Mar 29, 7:25 PM
Unknown Object (File)
Sun, Mar 24, 2:52 PM
Unknown Object (File)
Feb 21 2024, 8:41 PM
Unknown Object (File)
Feb 21 2024, 3:37 PM
Unknown Object (File)
Feb 20 2024, 11:08 PM
Unknown Object (File)
Feb 20 2024, 11:08 PM
Unknown Object (File)
Feb 20 2024, 11:08 PM
Subscribers

Details

Summary

Introduce two new columns to the messages table to handle messages being pinned.

  1. pinned - represents a boolean 0 (unpinned) or 1 (pinned)
  2. pin_time - the time the message was pinned. Necessary so we can display pins from newest to oldest.
  3. Add an index onto pinned since we'll be querying on messages where pinned = 1 when fetching all pins.

I've also added a migration to update messages.

Linear: https://linear.app/comm/issue/ENG-3182/introduce-columns-in-messages-table-to-support-pinned-messages

Test Plan

Tested the ALTER TABLE` statements locally in TablePlus individually to confirm the columns gets created and no syntax errors are present.

ALTER TABLE messages
   ADD COLUMN IF NOT EXISTS pinned tinyint(1) UNSIGNED NOT NULL DEFAULT 0,
   ADD COLUMN IF NOT EXISTS pin_time bigint(20) DEFAULT NULL,
   ADD INDEX IF NOT EXISTS pinned (pinned);

Also checked the table after the migration was successful:

Screenshot 2023-03-20 at 2.58.02 PM.png (198×2 px, 64 KB)

Diff Detail

Repository
rCOMM Comm
Lint
No Lint Coverage
Unit
No Test Coverage

Event Timeline

rohan edited the test plan for this revision. (Show Details)
rohan edited the summary of this revision. (Show Details)
rohan edited the test plan for this revision. (Show Details)
rohan edited the summary of this revision. (Show Details)
rohan requested review of this revision.Mar 1 2023, 7:43 AM

While I agree that it might be better database design to create a separate table, it will complicate things on the SQLite side.

We persist messageStore.messages and threadStore in SQLite on the native client, and it would probably make the most sense to store pinned_messages in the SQLite threads table instead of introducing a whole new table and set of “ops.” That would introduce inconsistency between the MariaDB and SQLite database… which isn’t the end of the world, but could be avoided if we stored pinned_messages in the threads table?

In D6924#206038, @atul wrote:

While I agree that it might be better database design to create a separate table, it will complicate things on the SQLite side.

We persist messageStore.messages and threadStore in SQLite on the native client, and it would probably make the most sense to store pinned_messages in the SQLite threads table instead of introducing a whole new table and set of “ops.” That would introduce inconsistency between the MariaDB and SQLite database… which isn’t the end of the world, but could be avoided if we stored pinned_messages in the threads table?

We could store pinned_messages in the threads table if we really want to. In terms of storing the messages, I think each time we get a new pin for a thread, we'd run an UPDATE and CONCAT the existing pinned_messages with the new message ID. Essentially looking like a long CSV in the column 103438, 103928, 104948 etc.

We could then query the column and split it into an array of IDs and reverse it to have it from newest to oldest pins.

It definitely is less ideal, especially when having to remove a messageID from some index in the list when unpinning a message. I'm a little wary of the performance of it when having to find a messageID in a long list of pinned_messages and update the string accordingly with REPLACE

If you feel like it's better to introduce a new column to the threads table though, I'm happy to change that

tomek requested changes to this revision.Mar 7 2023, 4:25 AM

We persist messageStore.messages and threadStore in SQLite on the native client, and it would probably make the most sense to store pinned_messages in the SQLite threads table instead of introducing a whole new table and set of “ops.” That would introduce inconsistency between the MariaDB and SQLite database… which isn’t the end of the world, but could be avoided if we stored pinned_messages in the threads table?

In which way does it affect the database on the keyserver side?
It's really hard for me to find some advantages of having denormalized pins column on the server. It will make any querying based on pins a lot harder than it should be.

On the other hand creating a new table also has some issues from the design's perspective. For example, thread column is a function of messageID because based on message id we can uniquely identify a thread. So this new table isn't e.g. a joining table but instead it depends on just messages table and could be replaced by new columns (is_pinned and pin_timestamp) on it.

I'm not sure, at this point, what is the best design here, but I think that we should spent a little more time figuring it out.

keyserver/src/database/setup-db.js
96–97 ↗(On Diff #23341)

These are both ids: why do we use ID suffix for just one of them?

This revision now requires changes to proceed.Mar 7 2023, 4:25 AM
In D6924#207293, @tomek wrote:

On the other hand creating a new table also has some issues from the design's perspective. For example, thread column is a function of messageID because based on message id we can uniquely identify a thread. So this new table isn't e.g. a joining table but instead it depends on just messages table and could be replaced by new columns (is_pinned and pin_timestamp) on it.

I'm not sure, at this point, what is the best design here, but I think that we should spent a little more time figuring it out.

Yeah, that's a valid point. An alternative approach, like you suggest, is to run a migration to update the messages table to include two new columns, either a boolean or 0/1 to signify is_pinned, and pin_time.

thread is already indexed and it shouldn't be a problem to add an index onto is_pinned since we may be query'ing on it (not sure yet).

I'm happy to go down this route as well, it prevents the creation of a new table and doesn't include the complexity of storing pinned messages data within the threads table.

keyserver/src/database/setup-db.js
96–97 ↗(On Diff #23341)

I tried following convention, and we use thread to represent the thread in each table. I didn't really have a better name for the message id, maybe just message or id.

Introduce two columns in messages

rohan retitled this revision from [keyserver] Introduce a pinned_messages table to [keyserver] Introduce columns in messages table to support pinned messages.Mar 20 2023, 11:58 AM
rohan edited the summary of this revision. (Show Details)
rohan edited the test plan for this revision. (Show Details)

How the pinning will be saved on client's side? Do we plan to have it included in a message info? If that's the case, do we plan to update all the message infos saved on all the clients?

keyserver/src/database/migration-config.js
226

What is the purpose of this index? When are we going to use it?

If we have a plan to use it, should we also want to include a thread id in it?

keyserver/src/database/setup-db.js
93–94

pinned is a little redundant as non-null pin_time means that a message is pinned, but we can keep it.

This revision is now accepted and ready to land.Mar 21 2023, 9:54 AM
In D6924#211499, @tomek wrote:

How the pinning will be saved on client's side? Do we plan to have it included in a message info? If that's the case, do we plan to update all the message infos saved on all the clients?

I'm currently introducing a new message type and a message spec for pinned messages, so we'd have messageContentForClientDB. From my understanding, I can support both content for server and client, but feel free to correct me if I'm wrong.

keyserver/src/database/migration-config.js
226

I'ved added an index on pinned since when we fetch all message pins for a thread, we'd be querying on message.pinned = 1, so I thought it'd be good to have indexed. threadID is already indexed it seems like.

The query is specifically in D7110.

In D6924#211575, @rohan wrote:
In D6924#211499, @tomek wrote:

How the pinning will be saved on client's side? Do we plan to have it included in a message info? If that's the case, do we plan to update all the message infos saved on all the clients?

I'm currently introducing a new message type and a message spec for pinned messages, so we'd have messageContentForClientDB. From my understanding, I can support both content for server and client, but feel free to correct me if I'm wrong.

It seems like I wasn't clear enough in my comment, so let me explain it differently. Correct me in any place where I'm wrong. So on the client side we would want to display three things:

  1. A robotext message in a thread telling that a message was pinned
  2. A pin indicator next to a message
  3. A list of pinned messages

As far as I understand, what you're describing handles only the 1st point. How do we plan to handle 2nd and 3rd?

keyserver/src/database/migration-config.js
226

Have you tested that both indexes are used in this query (using EXPLAIN)? I would be surprised if the engine uses both of them - more likely case is that the first index is used and then a scan is performed. The solution to that is using a compound index - consisting of two columns.

In D6924#211756, @tomek wrote:

It seems like I wasn't clear enough in my comment, so let me explain it differently. Correct me in any place where I'm wrong. So on the client side we would want to display three things:

  1. A robotext message in a thread telling that a message was pinned
  2. A pin indicator next to a message
  3. A list of pinned messages

As far as I understand, what you're describing handles only the 1st point. How do we plan to handle 2nd and 3rd?

Ah I understand now, sorry. Yeah you're right, those are the three things we want to display client side.

  1. Robotext: Will be handled by introducing a new message type and spec
  2. Pin indicator: I think this is the one I'm a little unsure on since I've not begun working on the client-side yet so I wasn't sure on the best way to handle this. I'm not sure if updating RawMessageInfo to support something like RawPinnedMessageInfo and MessageInfo to support PinnedMessageInfo is the best way
  3. A list of pinned messages: D7110 and D7112 should handle this, I can call the endpoint to retrieve all of the pinned messages for a given thread.
keyserver/src/database/migration-config.js
226

Ah I suspect you're right, it looks like it chooses one of the indexes.

I can modify the ALTER TABLE to create a compound index, and the migration as well

keyserver/src/database/migration-config.js
226

Yeah I think you should do this (no need to wait on @tomek to reply)

Create compound index, set my DB version back to 20 and re-run the migration to confirm no errors are present, and use EXPLAIN on the query in D7110 to confirm that the query uses the newly created thread_pinned index

In D6924#211911, @rohan wrote:
In D6924#211756, @tomek wrote:

It seems like I wasn't clear enough in my comment, so let me explain it differently. Correct me in any place where I'm wrong. So on the client side we would want to display three things:

  1. A robotext message in a thread telling that a message was pinned
  2. A pin indicator next to a message
  3. A list of pinned messages

As far as I understand, what you're describing handles only the 1st point. How do we plan to handle 2nd and 3rd?

Ah I understand now, sorry. Yeah you're right, those are the three things we want to display client side.

  1. Robotext: Will be handled by introducing a new message type and spec
  2. Pin indicator: I think this is the one I'm a little unsure on since I've not begun working on the client-side yet so I wasn't sure on the best way to handle this. I'm not sure if updating RawMessageInfo to support something like RawPinnedMessageInfo and MessageInfo to support PinnedMessageInfo is the best way
  3. A list of pinned messages: D7110 and D7112 should handle this, I can call the endpoint to retrieve all of the pinned messages for a given thread.
  1. Makes sense!
  2. We should spend some time thinking about it as it might affect the backend side. We have a lot of options, but most of them aren't good. Especially, we should consider how are we going to inform client A that a message was pinned on client B.
  3. Here we also have some decisions to make:
    1. Do we plan to fetch pinned messages every time a user opens pinned messages view?
    2. Do we plan to fetch all the messages at once or we're going to fetch them while scrolling?
    3. Do we plan to have a couple of pinned messages on client and fetch more if necessary?
    4. Do we plan to update a view when a new message got pinned on other client?
    5. Do we plan to display pinned messages optimistically (so when a user pins a message and opens the view, should we display a message as pinned before server saves the pin)?

Answering these is crucial because it affects how the API looks like. We probably should prefer simplifying the solution but it still requires being sure what the requirements are.

In D6924#212240, @tomek wrote:
In D6924#211911, @rohan wrote:

Ah I understand now, sorry. Yeah you're right, those are the three things we want to display client side.

  1. Robotext: Will be handled by introducing a new message type and spec
  2. Pin indicator: I think this is the one I'm a little unsure on since I've not begun working on the client-side yet so I wasn't sure on the best way to handle this. I'm not sure if updating RawMessageInfo to support something like RawPinnedMessageInfo and MessageInfo to support PinnedMessageInfo is the best way
  3. A list of pinned messages: D7110 and D7112 should handle this, I can call the endpoint to retrieve all of the pinned messages for a given thread.
  1. Makes sense!
  2. We should spend some time thinking about it as it might affect the backend side. We have a lot of options, but most of them aren't good. Especially, we should consider how are we going to inform client A that a message was pinned on client B.
  3. Here we also have some decisions to make:
    1. Do we plan to fetch pinned messages every time a user opens pinned messages view?
    2. Do we plan to fetch all the messages at once or we're going to fetch them while scrolling?
    3. Do we plan to have a couple of pinned messages on client and fetch more if necessary?
    4. Do we plan to update a view when a new message got pinned on other client?
    5. Do we plan to display pinned messages optimistically (so when a user pins a message and opens the view, should we display a message as pinned before server saves the pin)?

Answering these is crucial because it affects how the API looks like. We probably should prefer simplifying the solution but it still requires being sure what the requirements are.

  1. Yeah that makes sense. I'll spend some time thinking about it today and tomorrow.
  2. My thoughts:
    1. At the moment yet, though I don't think it's particularly difficult if we'd want to implement some sort of client-side caching once I begin working on the client. I'd prioritize that after the bulk of the pinned messages is complete
    2. I'd plan on paginating the results on scroll (similar to how the media gallery fetches upon scroll),
    3. Ideally, yeah since I don't want to fetch an unknown amount of (potentially large) pinned messages. I'd say the plan is to fetch an initial amount once the chat loads or when the modal is first opened.
    4. I don't think we should, I think we should include the new pinned messages only when the modal is reopened, because otherwise the view could be quite annoying if many pinned messages are coming in and moving around the view
    5. I don't have a strong opinion - it does add complexity I think and once the core keyserver implementation is complete I can work on that. Open to go either way on it.

The current fetchPinnedMessageInfos query doesn't have the ability to paginate, but it can always be added in later. We could initially fetch all of the pinned messages at once from the server, but have a task to paginate the fetching.

Separately, I'm a bit confused about how the client will determine eg. how many messages are pinned for a given chat. Is this info going to be fetched on-demand when the user opens the chat? (In which case it would appear "dynamically" after being fetched.) Or are we looking to "cache" this information for each thread, so that we correctly display it when opened?

I think it would be good to avoid having to fetch anything "dynamically" when a chat is opened. Depending on what information we show by default (I don't recall the designs exactly but it might just be a number of pinned messages), we might want to consider modifying ThreadInfo to store this information.

The current fetchPinnedMessageInfos query doesn't have the ability to paginate, but it can always be added in later. We could initially fetch all of the pinned messages at once from the server, but have a task to paginate the fetching.

Yeah agreed, I've made a task to track: https://linear.app/comm/issue/ENG-3397/add-pagination-to-fetching-message-pins-for-a-thread

Separately, I'm a bit confused about how the client will determine eg. how many messages are pinned for a given chat. Is this info going to be fetched on-demand when the user opens the chat? (In which case it would appear "dynamically" after being fetched.) Or are we looking to "cache" this information for each thread, so that we correctly display it when opened?

I think it would be good to avoid having to fetch anything "dynamically" when a chat is opened. Depending on what information we show by default (I don't recall the designs exactly but it might just be a number of pinned messages), we might want to consider modifying ThreadInfo to store this information.

That's a good point, it probably would be good to store the number in ThreadInfo. I've similarly made a follow-up task: https://linear.app/comm/issue/ENG-3396/store-the-number-of-pinned-messages-in-threadinfo

Makes sense! Thanks for clarifying and answering the questions!

Rebase and resolve conflicts