Query - Sending public and private messages to users - mysql

Need to implement the delivery of public messages for all users and personal messages.
Simplified tables:
messages
+---------------+---------------------------+
| Field | Type |
+---------------+---------------------------+
| id | int(10) unsigned |
| admin_id | int(10) unsigned |
| type | enum('public','targeted') |
| subject | text |
+---------------+---------------------------+
messages_read_status
+----------------+---------------------------------+
| Field | Type |
+----------------+---------------------------------+
| id | int(10) unsigned |
| message_id | int(10) unsigned |
| user_id | int(10) unsigned |
| status | enum('unread','read') |
+----------------+---------------------------------+
When sending public messages - in messages_read_status table records not created. For targeted messages - creating record with unread status. When message is read by user - setting read status.
Two filters on client side - Read/Unread. Message type does not matter for user.
Main task - create query to request Read or Unread messages for user, regardless of the type of message.
The main problem is to handle in query request for public unread messages, because there can be no records for them in messages_read_status . UNREAD query works properly while some user reads the message. Then new record in messages_read_status created and other users can't see this message any more. This situation I can't resolve. READ query works correctly.
SELECT messages.* FROM messages
LEFT JOIN messages_read_status
ON messages.id = messages_read_status.message_id
WHERE
{OTHER FILTERS} AND
(messages_read_status.`id` IS NULL OR (messages_read_status.`user_id` = $user_id AND messages_read_status.`message_status` = '$message_status'))
samlpe sql - http://sqlfiddle.com/#!2/d940d
Thank you

Way to get Unread (public + targeted)
SELECT messages.* FROM messages
LEFT JOIN messages_read_status
ON (
messages.id = messages_read_status.message_id AND
messages_read_status.`user_id` = $user_id
)
WHERE
{OTHER FILTERS} AND
((messages_read_status.`id` IS NULL AND messages.type='public') OR messages_read_status.`status` = 'unread')
AND for Read using simpler rules
{OTHER FILTERS} AND messages_read_status.`user_id` = $user_id AND messages_read_status.`status` = 'read'

Related

Is a given sql statement vulnerable to a sql injection attack?

I'm building a sql statement like the one below in a rails app :-
bank_ids = params[:bank_ids] # comes from end user or simply, is a user input.
sql_string = "SELECT * FROM users WHERE bank_id IN (#{bank_ids});"
Is the sql statement above vulnerable to an injection attack, input 'bank_ids' is end user controlled.
Take as example a table designed to store a boolean value to tell if a user is admin or not (might not happend, but it's an example):
Table "public.users"
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
------------+--------------------------------+-----------+----------+-----------------------------------+----------+--------------+-------------
id | bigint | | not null | nextval('users_id_seq'::regclass) | plain | |
name | character varying | | | | extended | |
admin | boolean | | | | plain | |
bank_id | integer | | | | plain | |
If you receive something like this:
'1) or id IN (select id from users where admin = true'
And that's interpolated afterward, then the select clause asking for the admin users will retrieve data that otherwise wouldn't appear. The query would be executed as it's built:
select * from users where bank_id IN (1) or id IN (select id from users where admin = true)
Is better for you to rely on the ORM you have at hand and leave it to do the sanitization and proper bindings for you (it's one of the reasons why those tools exist). Using ActiveRecord for example would bind the passed values for you, without having to do much:
User.where(bank_id: '1) or id IN (select id from users where admin = true')
# ... SELECT "users".* FROM "users" WHERE "users"."bank_id" = $1 [["bank_id", 1]]

How to use GROUP BY which takes into account two columns?

I have a message table like this in MySQL.
+--------------------+--------------+------+-----+---------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------------+--------------+------+-----+---------------------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| subject | varchar(120) | NO | | NULL | |
| body | longtext | NO | | NULL | |
| sent_at | datetime | YES | | NULL | |
| recipient_read | tinyint(1) | NO | | 0 | |
| recipient_id | int(11) | NO | MUL | 0 | |
| sender_id | int(11) | NO | MUL | 0 | |
| thread_id | int(11) | NO | MUL | 0 | |
+--------------------+--------------+------+-----+---------------------+----------------+
Messages in a recepient's inbox are to be grouped by thread_id like this:
SELECT * FROM message WHERE recipient_id=42 GROUP BY thread_id ORDER BY sent_at DESC
My problem is how to take recipient_read into account so that each row in the result also show what is the recipient_read value of the last message in the thread?
In the original query, the ORDER BY is only satisfied after the GROUP BY operation. The ORDER BY affects the order of the returned rows. It does not influence which rows are returned.
With the non-aggregate expression in the SELECT list, it is indeterminate which values will be returned; the value of each column will be from some row in the collapsed group. But it's not guaranteed to be the first row, or the latest row, or any other specific row. The behavior of MySQL (allowing the query to run without throwing an error) is enabled by a MySQL extension.
Other relational databases would throw a "non-aggregate in SELECT list not in GROUP BY" type error with the query. MySQL exhibits a similar (standard) behavior when ONLY_FULL_GROUP_BY is included in sql_mode system variable. MySQL allows the original query to run (and return unexpected results) because of a non-standard, MySQL-specific extension.
The pattern of the original query is essentially broken.
To get a resultset that satisfies the specification, we can write a query to get the latest (maximum) sent_at datetime for each thread_id, for a given set of recipient_id (in the example query, the set is a single recipient_id.)
SELECT lm.recipient_id
, lm.thread_id
, MAX(lm.sent_at) AS latest_sent_at
FROM message lm
WHERE lm.recipient_id = 42
GROUP
BY lm.recipient_id
, lm.thread_id
We can use the result from that query in another query, by making in an inline view (wrap it in parens, and reference it in the FROM clause like table, assign an alias).
We can join that resultset to the original table to retrieve all of the columns from the rows that match.
Something like this:
SELECT m.id
, m.subject
, m.body
, m.sent_at
, m.recipient_read
, m.recipient_id
, m.sender_id
, m.thread_id
FROM (
SELECT lm.recipient_id
, lm.thread_id
, MAX(lm.sent_at) AS latest_sent_at
FROM message lm
WHERE lm.recipient_id = 42
GROUP
BY lm.recipient_id
, lm.thread_id
) l
JOIN message m
ON m.recipient_id = l.recipient_id
AND m.thread_id = l.thread_id
AND m.sent_at = l.latest_sent_at
ORDER
BY ...
Note that if (recipient_id,thread_id,sent_at) is not guaranteed to be unique, there is a potential that there will be multiple rows with the same "maximum" sent_at; that is, we could get more than one row back for a given maximum sent_at.
We can order that result however we want, with whatever expressions. That will affect only the order that the rows are returned in, not which rows are returned.
If you want the last message, you want filtering, not aggregation:
SELECT m.*
FROM message m
WHERE m.recipient_id = 42 AND
m.sent_at = (SELECT MAX(m2.sent_at)
FROM messages m2
WHERE m2.thread_id = m.thread_id
)
ORDER BY m.sent_at DESC;

Read notifications table

I am making a notifications table. That is structured like this:
notifications:
+----+------+----------------------+-----------------------------------+-----------+
| ID | user | notification | message | timestamp |
+----+------+----------------------+-----------------------------------+-----------+
| 1 | 6 | Denied acceptance | You have been denied for... | |
| 2 | 6 | Apreooved acceptance | You have been accepted... | |
| 3 | 0 | Open slots | there are still open slots for... | |
+----+------+----------------------+-----------------------------------+-----------+
And a table that indicates which notification was read by each user
notifications_read
+----+------+--------------+-----------+
| ID | user | notification | timestamp |
+----+------+--------------+-----------+
| 1 | 6 | 2 | |
+----+------+--------------+-----------+
If notification is meant only for one user, there is a user id under "user" in "notifications"- If the notification is meant for everybody I insert '0' under user.
I am trying to make a query that selects rows from "notifications", that is not read.
My current query is not ok:
SELECT *
FROM notifications
WHERE id NOT IN
(SELECT notification
FROM notification_read
WHERE user = $id) AND user = $id OR user = 0
So for above table it needs to select the rows with id 1 & 3
Thank you for the help!
If you want unread notifications for a particular user, then you are on the right track:
SELECT n.*
FROM notifications n
WHERE n.id NOT IN (SELECT nr.notification
FROM notification_read nr
WHERE nr.user = $id) AND
(n.user = $id OR n.user = 0);
I think the only issue is the parentheses for the logic around OR.
You can also write this using IN:
SELECT n.*
FROM notifications n
WHERE n.id NOT IN (SELECT nr.notification
FROM notification_read nr
WHERE nr.user = $id) AND
n.user IN (0, $id);
SELECT n.*
FROM notifications n
LEFT JOIN notifications_read nr ON nr.notification = n.id
WHERE
nr.id IS NULL
This will give you notifications that don't exist n the notifications_read table.
Left join will give you all rows in notifications and join rows from notifications_read that have a notification id. So when you filter by nr.id IS NULL you will return rows that exist in notifications but not in notifications_read.
Additionally, the LEFT JOIN will perform better.
A LEFT [OUTER] JOIN can be faster than an equivalent subquery because the server might be able to optimize it better—a fact that is not specific to MySQL Server alone.
https://dev.mysql.com/doc/refman/5.7/en/rewriting-subqueries.html

Message Table, Don't show messages to who deleted

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)
);

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.