Message Table, Don't show messages to who deleted - mysql

I have a mysql table named messages. It's structure below
id | sender_id | receiver_id | message | date | is_receiver_read | conversation_id
1 | 99 | 456 | hello | 2014 | 1 | 99x456
2 | 456 | 99 | hi) | 2014 | 0 | 99x456
When a sender or receiver delete messages from conversation i don't want to show to him messages from this conversation. How can do it?
When I add columns which named 'is_sender_delete' and 'is_receiver_delete' there is a problem. For first message sender is 99 but for second message sender is 456. When I update my table after an user press 'delete converstaion button' which column should I update?
If this structure is wrong, what is the alternative solution?

I think you need something like this...
CREATE TABLE subscriptions
(user_id INT NOT NULL
,conversation_id INT NOT NULL
,subscribed TINYINT NOT NULL DEFAULT 1
,PRIMARY KEY(user_id,conversation_id)
);

Related

How to handle friend request accept logic in database?

I am working on a simple social app and have a user_friend table which has both the user_id and friend_id as its composite keys. In the front end, the current user can look at other people's profiles and then click on add friend button which updates the user_friend table. For example, user with id 100 can view user with id 9's and 15's profiles and click on add button and then the user_friend table gets updated as
user_id: 100, friend_id 9
and
user_id: 100, friend_id 15
What is the best approach to handling such request? I was thinking creating a new table called request_table which has requester column which has the id for the user, accepter column which has the id for the friend and status column with accepted and pending. So, when requester clicks add friend button, the status gets updated to accepted which then updates the user_friend table to to reflect the change (by adding a new row user_id: 9, friend_id 100 in the above example).
Please advice if there are cleaner or better ways to do this.
I would make it much simpler than you are thinking .. Your current table looks like:
+--------------------------------+
| user_id | friend_id |
+--------------+-=---------------+
| 100 | 15 |
+--------------+-----------------+
| 100 | 9 |
+--------------+-----------------+
Add two columns .. requested and accepted:
+--------------------------------+-----------------+----------------+
| user_id | friend_id | requested | accepted |
+--------------+-=---------------+-----------------+----------------+
| 100 | 15 | 1 | 0 |
+--------------+-----------------+-----------------+----------------+
| 100 | 9 | 1 | 1 |
+--------------+-----------------+-----------------+----------------+
Although one could ASSUME that if the entry is in the table requested will always be 1 -- So really you only need to add the accepted column .. But you get the basic idea/principle.
NOTE if you need more statuses than just "accepted" like -- Say "blocked" or "suspended" etc etc you can create a third table and use the in a relational way.
+-----------------------------------------------------------------+
| user_firends (uf_id for indexing FASTER) |
+--------------------------------+-----------------+--------------+
| uf_id | user_id | friend_id | status |
+--------------+-=---------------+-----------------+--------------+
| 1 | 100 | 9 | 1 |
+--------------+-----------------+-----------------+--------------+
| 2 | 100 | 15 | 2 |
+--------------+-----------------+-----------------+--------------+
+---------------------------------------+
| statuses_table |
+------------------+--------------------+
| status_id | status |
+------------------+--------------------+
| 1 | requested |
+------------------+--------------------+
| 2 | accepted |
+------------------+--------------------+
| 3 | rejected |
+------------------+--------------------+
| 4 | blocked |
+------------------+--------------------+
many (users) to many (friends) with a users_friends "pivot" table AND
many (friends) to single (staus) with a direct insert of status in the column

How to design schema, display a message to it's owner, but if owner is unspecified, display to all user?

I have the schema like this,
message
+------------+----------+
| id | text |
+------------+----------+
| 1 | message1 |
| 2 | message2 |
| 3 | message3 |
+------------+----------+
user_message
+------------+-------------+
| user_id | message_id |
+------------+-------------+
| 1 | 1 |
| 1 | 2 |
| 2 | 2 |
+------------+-------------+
because the message3 is no owner, it owned to all user.
So, the user1 has message1, message2 and message3,
the user2 has message2 and message3.
And I need write the sql to query user1's messages,
SELECT
*
FROM
message AS a
JOIN
user_message AS b ON a.id = b.message_id AND b.user_id = 1
UNION ALL
SELECT
*
FROM
message AS a
LEFT JOIN
user_message AS b ON a.id = b.message_id
WHERE
b.user_id IS NULL
Does this design correct?
Or I should add the message3 to all users, like this?
+------------+-------------+
| user_id | message_id |
+------------+-------------+
| 1 | 1 |
| 1 | 2 |
| 2 | 2 |
| 1 | 3 |
| 2 | 3 |
+------------+-------------+
But if I have a new user, I wish the new user own message3, I need to write extra code to do this.
How do I do it correctly?
EDIT:
I made a mistake above, I lost a case is one user can has many messages.
As suggested by Neville Kuyt.
I like the "no surprises".
And I change the column name and schema to
message
+------------+----------+
| id | text |
+------------+----------+
| 1 | message1 |
| 2 | message2 |
| 3 | message3 |
+------------+----------+
user_message
+------------+-------------+-------------+
| id | user_id | message_id |
+------------+-------------+-------------+
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 2 | 2 |
| 4 | null | 3 |
+------------+-------------+-------------+
Now, the query will be
SELECT
*
FROM
user_message AS a
JOIN
message AS b ON a.message_id = b.id
WHERE
user_id = 1 OR user_id IS NULL;
"Correct" is a tricky thing to get right in schema design. I generally favour the "no surprises" approach - someone should be able to understand what's going on by looking at the schema and the data, without reading the documentation. I also favour "don't repeat yourself".
You offer two candidate solutions.
The first solution contains a surprise - columns with the name "id" are usually the primary key for a table; in this case, the "id" column is actually a foreign key to the "users" table. To reduce this level of surprise, I'd create the column "user_id" to contain the foreign key. If "user_id" is also the primary key in your business domain, you can just rename the column.
The second surprise is that the column contains foreign key data to the user table which don't exist, but which invoke special behaviour - they are sent to all users. A less surprising solution would be for that value to be "null", rather than a non-existent value. When you create user 3, you update the appropriate record in message.
Your schema then becomes
message
+----------------+----------+
| id |user_id | text |
+-------+--------+----------+
| 1 | 1 | message1 |
| 2 | 2 | message2 |
| 3 | null | message3 |
+-------+--------+----------+
Your second option contains another surprise - data in "messages" changes as a side-effect of a change to "user" (when you create a new user, you have to delete all the messages to other users with the ID of that user). It has the benefit of being explicit - every message/user combination is stored, but I don't like the side-effect.
You can use exists and not exists:
select m.*
from message m
where exists (select 1
from user u
where u.message_id = m.id and
u.user_id = 1
) or
not exists (select 1
from user u
where u.message_id = m.id
);

mysql: delete rows between repeating specific values

SQL rookie here. I have a broken punch in/out type table with millions of records fed by a legacy bad app that did not check for previous logins/logouts before merrily inserting another duplicate record. The app is fixed but I need to sanitize the table to retain the historical data so it can be fed into future reports.
In a nutshell, I'm trying to keep each minimum login row followed by the next minimum logout row and discard everything else between. The bad app allowed both duplicate logins AND logouts... grrrr.
Every "duplicate row" type question I've searched for here doesn't seem to apply to this type of grouping situation. From being a long time SO lurker I know you guys would like to see what I've already tried but have already tried tens of goofy query attempts that aren't coming close. Any guidance would be greatly appreciated.
Here's the table and what I'm trying to do and the fiddle with schema
+---------------------+-------+-------------+---------------+
| calldate | agent | etype | uniqueid |
+---------------------+-------+-------------+---------------+
| 2018-02-02 19:26:47 | 501 | agentlogin | 1517599607.71 |
| 2018-02-02 19:26:55 | 501 | agentlogin | 1517599615.72 |<-- delete
| 2018-02-02 19:27:32 | 501 | agentlogoff | 1517599652.73 |
| 2018-02-02 19:27:43 | 501 | agentlogin | 1517599663.74 |
| 2018-02-02 19:28:24 | 501 | agentlogoff | 1517599704.75 |
| 2018-02-02 19:29:02 | 501 | agentlogoff | 1517599742.76 |<-- delete
| 2018-02-02 19:29:39 | 501 | agentlogoff | 1517599778.77 |<-- delete
| 2018-02-02 19:34:54 | 501 | agentlogin | 1517600094.80 |
| 2018-02-02 19:35:23 | 501 | agentlogin | 1517600122.81 |<-- delete
| 2018-02-02 19:35:49 | 501 | agentlogin | 1517600149.82 |<-- delete
| 2018-02-02 19:36:04 | 501 | agentlogoff | 1517600164.83 |
| 2018-02-02 19:36:08 | 501 | agentlogoff | 1517600168.84 |<-- delete
+---------------------+-------+-------------+---------------+
I would create a copy of the table with an auto_increment column. This way you can compare two neighbor rows more easily and more efficiently.
Find in the new table the rows which have the same agent and etype as in the previous row and join the result with the original table using the unique column in a DELETE statement.
create table tmp (
`id` int unsigned auto_increment primary key,
`calldate` datetime,
`uniqueid` varchar(32),
`agent` varchar(80),
`etype` varchar(80)
) as
select null as id, calldate, uniqueid, agent, etype
from test
order by agent, calldate, uniqueid
;
delete t
from tmp t1
join tmp t2
on t2.id = t1.id + 1
and t2.agent = t1.agent
and t2.etype = t1.etype
join test t on t.uniqueid = t2.uniqueid;
drop table tmp;
Demo: http://sqlfiddle.com/#!9/3e96b/2
You should however first have an index on uniqueid.
Here you go:
select calldate,agent,etype,uniqueid
from test as t1
where not exists
(select *
from
test as t2
where t2.agent=t1.agent
and t2.etype=t1.etype
and t2.uniqueid<t1.uniqueid
and t2.uniqueid>ifnull((select max(uniqueid )
from test t3
where t3.agent=t1.agent
and t3.etype<>t1.etype
and t3.uniqueid<t1.uniqueid),0)
)
order by uniqueid;
http://sqlfiddle.com/#!9/149802/16

Get distinct results from several tables

I need to implement mysql query to calculate space used by user's mailbox.
A message thread may have multiple messages (reply, follow up) by 2 parties
(sender/recipient) and is tagged with one or more tags (Inbox, Sent etc.).
The following conditions have to be met:
a) user is either recipient OR author of the message;
b) message IS TAGGED by any of the tags: 1,2,3,4;
c) distinct records only, ie if the thread, containing messages is tagged with
more than one of the 4 tags (for example 1 and 4: Inbox and Sent) the calculation
is done on one tag only
I have tried the following query but I am not able to get distinct values - the
subject/body values are duplicated:
SELECT SUM(LENGTH(subject)+LENGTH(body)) AS sum
FROM om_msg_message omm
JOIN om_msg_index omi ON omm.mid = omi.mid
JOIN om_msg_tags_index omti ON omi.thread_id = omti.thread_id AND omti.uid = user_id
WHERE (omi.recipient = user_id OR omi.author = user_id) AND omti.tag_id IN (1,2,3,4)
GROUP BY omi.mid;
Structure of the tables:
om_msg_message - fields subject and body are the ones to be calculated
+--------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+------------------+------+-----+---------+----------------+
| mid | int(10) unsigned | NO | PRI | NULL | auto_increment |
| subject | varchar(255) | NO | | NULL | |
| body | longtext | NO | | NULL | |
| timestamp | int(10) unsigned | NO | | NULL | |
| reply_to_mid | int(10) unsigned | NO | | 0 | |
+--------------+------------------+------+-----+---------+----------------+
om_msg_index
+-----+-----------+-----------+--------+--------+---------+
| mid | thread_id | recipient | author | is_new | deleted |
+-----+-----------+-----------+--------+--------+---------+
| 1 | 1 | 1392 | 1211 | 0 | 0 |
| 2 | 1 | 1211 | 1392 | 1 | 0 |
+-----+-----------+-----------+--------+--------+---------+
om_msg_tags_index
+--------+------+-----------+
| tag_id | uid | thread_id |
+--------+------+-----------+
| 1 | 1211 | 1 |
| 4 | 1211 | 1 |
| 1 | 1392 | 1 |
| 4 | 1392 | 1 |
+--------+------+-----------+
Here's another solution:
SELECT SUM(LENGTH(omm.subject) + LENGTH(omm.body)) as totalLength
FROM om_msg_message omm
JOIN om_msg_index omi
ON omi.mid = omm.mid
AND (omi.recipient = user_id OR omi.author = user_id)
JOIN (SELECT DISTINCT thread_id
FROM om_msg_tags_index
WHERE uid = user_id
AND tag_id IN (1, 2, 3, 4)) omti
ON omti.thread_id = omi.thread_id
I'm assuming that:
user_id is a parameter marker/host variable, being queried for an individual user.
You want the total of all messages per user, not the total length of each message (which is what the GROUP BY clause in your version was getting you).
That mid in both om_msg_message and om_msg_index is unique.
So, your problem is the IN clause. I'm not a MYSQL guru, but in T-SQL you could change it to have a where clause on a subquery that contained an EXISTS so your join didn't pop out two rows. You need to compensate for the fact that you have two rows with different tagID's associated with each row of your primary join data.
The way I could do it cross-platform would be with four left-joins that linked tables then demanded a non-null value for 1, 2, 3, or 4. Fairly inefficient; I'm sure there's a better way to do it in MySQL, but now that you know what the problem is you might know a better solution.

Private Message Database Design

Alright, so I think I'm pretty close to having what I need, but I'm unsure about a couple of things:
TABLE messages
message_id
message_type
sender_id
timestamp
TABLE message_type
message_type_code (1, 2, 3)
name (global, company, personal)
TABLE message_to_user
message_id
receiver_id
status (read/unread)
Goals:
Be able to send GLOBAL messages to all users.
Send PERSONAL messages between 1 or more users.
Determine if any of these messages have been read or not by the the receiver.
Questions:
Does my schema take care of all that it needs to?
What would a sample SQL query look like to populate someones inbox, bringing in GLOBAL messages as well as PERSONAL messages - I'd like to be able to determine which is which for the UI.
And please feel free to add to my schema if you feel it would benefit.
Schema looks like it will work. Should probably have a Created date too. There's no way to know if you've read a global message though without creating entries for everyone.
Here's some SQL:
SELECT M.*, MTU.*
FROM messages M
LEFT JOIN message_to_user MTU ON MTU.message_id=M.message_id
WHERE MTU.receiver_id={$UserID} OR M.message_type={$GlobalType}
ORDER BY M.created_on DESC
[EDIT]
Problem: Every user needs to have their own unique "read" status for global e-mails. You probably also want to give them the ability to "delete"/hide this e-mail so they don't have to be looking at it all the time. There is no way around this without creating either a row for each e-mail as it's going out, which is probably taxing to do that many INSERTS all at once...or better yet, don't create a status until it's read. This way, INSERTS for global e-mails will only occur when the message is read.
messages
message_id
message_type
sender_id
timestamp
message_recipient
message_id
user_id
message_status
message_status_id
message_id
user_id
is_read
read_datetime
is_deleted
deleted_datetime
SELECT M.*, MR.*, MS.*
FROM messages M
LEFT JOIN message_recipient MR ON MR.message_id=M.message_id
LEFT JOIN message_status MS ON MS.message_id=M.message_id
WHERE
(MS.message_status_id IS NULL OR MS.is_deleted = 0)
(MR.user_id={$UserId} OR M.message_type={$GlobalType})
ORDER BY M.timestamp DESC
[EDIT]
Whether to use message_type as a DB table or simply as settings within your code is partly a personal preference and partly your needs. If you need to query the DB and see the text "personal" and "global" directly from your query, then you want to use the message_type table. However, if you only need the "type" to handle your business logic, but don't need to see it in query results, then I would go with an "Enum" style approach. Enums are a C# thing...in PHP, the closest you've got is a class with constants...something like:
class MessageTypes {
public const Global = 0;
public const Personal = 1;
}
So, your query would be: WHERE ... message_type=".MessageTypes::Global."...
The one method can be to separate the global messages from the personal messages as I think you have tried to do already.
To effectively get a read status for a global message, you would need to add a table with a composite key containing the global_message_id and user_id together.
messages_tbl
- message_id | int(11) | Primary Key / Auto_Increment
- message_type | int(11)
- sender_id | int(11) | FK to sender
- receiver_id | int(11) | FK to receiver
- status | int(1) | 0/1 for Unread / Read
- message | text
- date | datetime
global_message_tbl
- g_message_id | int(11) | Primary Key / Auto_Increment
- g_message_type | int(11)
- sender_id | int(11) | FK to sender
- date | datetime
global_readstatus_tbl
- user_id | int(11) | Primary Key
- g_message_id | int(11) | Primary Key
- date | datetime
Alternatively merge the messages_tbl and global_message_tbl so they each user is sent a global message personally in a loop. This reduces your schema right down to one table.
messages_tbl
- message_id | int(11) | Primary Key / Auto_Increment
- sender_id | int(11) | FK to sender
- receiver_id | int(11) | FK to receiver
- status | int(1) | 0/1 for Unread / Read
- message_type | varchar(8) | Personal / Global / Company
- message | text
- date | datetime
- type | varchar(8)
If you want the ability to normalise your table a bit better, and make it easier to add message types in the future, move message_type back into its own table again, and make message_type a FK of the message_type_id
message_type_tbl
- message_type_id | int(11) | Primary Key / Auto_Increment
- message_type | varchar(8) | Personal / Global / Company
Update - Sample Table (1 Table)
message_tbl
message_id | message_type | sender_id | receiver_id | status | message | datetime
1 | personal | 2 | 3 | read | foobar | 12/04/11 00:09:00
2 | personal | 2 | 4 | unread | foobar | 12/04/11 00:09:00
3 | personal | 3 | 2 | unread | barfoo | 12/04/11 02:05:00
4 | global | 1 | 2 | unread | gmessage | 13/04/11 17:05:00
5 | global | 1 | 3 | unread | gmessage | 13/04/11 17:05:00
6 | global | 1 | 4 | read | gmessage | 13/04/11 17:05:00
user_tbl
user_id | name
1 | Admin
2 | johnsmith
3 | mjordan
4 | spippen
The above assumes users 2, 3 and 4 are general users sending messages to each other, user 1 is the admin account that will be used to send global messages (delivered directly to each user individually) allowing you to see the same information as if it were a personal message.
To send a global message in this format you would simply loop over the users table to obtain all the ID's you want to send the global message out to, then simply INSERT the rows for each user in the messages_tbl.
If you don't anticipate your users sending millions of messages a day as well as regular global messages to millions of users then the number of rows shouldn't be an issue. You can always purge old read messages from users by creating a cleanup script.