How to properly use Wildcard with CONCAT - php

(Spoiler: The Title has nothing to do with what is wrong with the code.)
I'm creating a live-search system just to show the user possible event types already listed on my website. During my speculations I may have an error with Wildcard binding which I'm unable to see.
I tried using different types of "WHERE LIKE" statements, and most of them didn't work at all. Such as I tried using placeholder query (question mark) and that did not work at all. If I ran this query manually on my database I will get results which I'm expecting.
This is how my code looks, the variable $q is obtained using $_GET method.
$query = $pdo->prepare('SELECT DISTINCT EventCategory FROM Events
WHERE EventCategory LIKE CONCAT(\'%\',:q,\'%\')');
$query->bindParam(":q", $q);
$query->execute();
$row = $query->fetch(PDO::FETCH_ASSOC);
while ($row = $query->fetchObject()) {
echo "<div> $row->EventCategory </div>";
}
The expected results would be: If the $q is equal to n, Meeting and Nightlife is returned. When $q is equal to ni, then Nightlife is only returned.
The search is NOT CASE SENSITIVE, N and n is treated equally.
The SHOW CREATE TABLE Events query returned the following:
CREATE TABLE `Events` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`Name` varchar(100) NOT NULL,
`Image` varchar(600) NOT NULL,
`Date` date NOT NULL,
`Description` varchar(1200) NOT NULL,
`SpacesAvailable` int(11) NOT NULL,
`EventCategory` varchar(50) NOT NULL,
`Trending` varchar(30) DEFAULT NULL,
`TrendingID` int(255) NOT NULL,
`Sale` int(255) NOT NULL,
PRIMARY KEY (`ID`)
)DEFAULT CHARSET=latin1
Images to show the operation of the website: https://imgur.com/a/yP0hTm3
Please if you are viewing the images the view from bottom to top. Thanks

I suspect the default collation in your EventCategory column is case-sensitive. That's why Ni and ni don't match in Nightlife.
Try this query instead.
'SELECT DISTINCT EventCategory FROM Events WHERE EventCategory COLLATE utf8_general_ci LIKE CONCAT(\'%\',:q,\'%\')'
Or, if your column's character set is not unicode but rather iso8859-1, try this:
'SELECT DISTINCT EventCategory FROM Events WHERE EventCategory COLLATE latin1_general_ci LIKE CONCAT(\'%\',:q,\'%\')'
This explains how to look up the available character sets and collations on MySQL.
How to change collation of database, table, column? explains how to alter the default collation of a table or a column. It's generally a good idea because collations are baked into indexes.

The problem is not in LIKE, but in PHP and PDO. Stare at the 3 conflicting uses of $row in your code:
$row = $query->fetch(PDO::FETCH_ASSOC);
while ($row = $query->fetchObject()) {
echo "<div> $row->EventCategory </div>"; }
Then review the documentation and examples. (Sorry, I'm not going to feed you the answer; you need to study to understand it.)

In complement to the comprehensive answer by O.Jones, another, simpler solution would be to just perform a case-insensitive search, like :
'SELECT DISTINCT EventCategory
FROM Events
WHERE UPPER(EventCategory) LIKE CONCAT(\'%\',UPPER(:q),\'%\')'

Related

Getting the corresponding column in a different table MySQL

Is there a way where in I could use the sub_name, instead of the sub_id?
This is my code..
echo "<table border=1 align=center><tr class=style2><td>Date<td>Student Name <td> Subject <td> Score";
$sortQuery = mysql_query("select * from mst_adminresult",$cn) or die(mysql_error());
while($row=mysql_fetch_row($sortQuery))
{
echo "<tr class=style8><td>$row[5]<td>$row[1] <td align=center> $row[4] <td align=center> $row[3]/20";
}
echo "</table>";
this code shows a result of all the exam taken. In the Subject it's output is the subject ID. I'm just wondering if there is any way I could get its subject name. The subject name is in different table.
adminresult_tbl
CREATE TABLE `mst_adminresult` (
`adminResultID` int(5) NOT NULL,
`login` varchar(20) NOT NULL,
`test_id` varchar(20) NOT NULL,
`score` int(3) NOT NULL,
`test_date` date NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
subjecet_tbl
CREATE TABLE `mst_subject` (
`sub_id` int(5) NOT NULL,
`sub_name` varchar(25) DEFAULT NULL,
`sub_desc` text
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
test_tbl
CREATE TABLE `mst_test` (
`test_id` int(5) NOT NULL,
`sub_id` int(5) DEFAULT NULL,
`test_name` varchar(30) DEFAULT NULL,
`total_que` varchar(15) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
The flow of the program is, when the user takes an exam, the user will be redirected to a page where all the subjects are listed, then the user will choose the subject she wants to take, after that, a list of tests will show, then the user will choose again and that's when the user can have the quiz.
In the list of subjects, I've called the subject_tbl. In the list of test, the test_tbl. When the user is finished taking the exam, it will store in the adminresult_tbl
JOIN the 3 tables. Try this:
SELECT * FROM mst_adminresult m
JOIN test_tbl t ON t.test_id = m.test_id
JOIN subject_tbl s ON s.sub_id = t.sub_id;
As depicted by #PrinceG, you may use the JOIN syntax to read values from other tables. To stay in the format of your coding, the SQL statement should read something like this:
SELECT ar.*, s.sub_name
FROM mst_adminresult AS ar
LEFT JOIN mst_test AS t ON t.test_id = ar.test_id
LEFT JOIN mst_subject AS s ON s.sub_id = t.sub_id;
Doing so will allow you to access the subject's name via $row[6].
However, also note that your database modelling is quite risky to do so:
You did not specify any primary key on your master data (e.g. mst_subject and mst_test) tables. This may lead to severe data inconsistencies if not handled carefully. Introducing primary keys are a very good habit for good reasons - for example see http://www.mysqltutorial.org/mysql-primary-key/
There is not referential integrity set up for your tables, although the underlying DB engine InnoDB supports it. Thus, it may happen - for instance - that you have a test_id of 5 in mst_adminresult, which does not have a corresponding record in mst_test. As the statement above uses LEFT JOINs, this would not suppress your record in the resultset returned, but a NULL value would show up in PHP. However, I doubt that this is intentional in your case. Instead, I would assume that it was a better approach that the DB would raise an error already when inserting a record into mst_adminresult for which no test is provided so far in mst_test.
The same pattern also applies to mst_subject in regards to mst_test.
In sum, you may find articles like that one on ER Modeling interesting.

Creating dynamic tables with MySQLi securely

I want to be able to create dynamic tables, for custom user surveys... like survey monkey... how would I go about create something like that?
Because I want to give the ability to the user to create the survey, with different amount of text fields, and different a option fields... I would need to create a custom table for each survey.
Would something like this be possible?
<?php
$table_name = 'survey_'.$_POST['surveyid'];
$query = 'CREATE TABLE ? (
`responseid` INT NOT NULL AUTO_INCREMENT,
`textarea1` TEXT NULL,
`textarea2` TEXT NULL,
`textarea3` VARCHAR(255) NULL,
`drop_down1` VARCHAR(255) NULL,
`drop_down2` VARCHAR(255) NULL,
`bool1` BIT NULL,
`bool2` BIT NULL,
PRIMARY KEY (`responseid`))';
if($stmt = $mysqli->prepare($query)){
$stmt->bind_param('s', $table_name);
$stmt->execute();
$stmt->close();
}else die("Failed to prepare");
?>
The above example comes back with "Failed to prepare", because I don't think I can prepare a table name... is there another work around using mysqli?
if(ctype_digit($_POST['surveyid']) && $_POST['surveyid']>0){
$table_name = 'survey_'.$_POST['surveyid'];
$query = 'CREATE TABLE '.$table_name.' (
`responseid` INT NOT NULL AUTO_INCREMENT,
`textarea1` TEXT NULL,
`textarea2` TEXT NULL,
`textarea3` VARCHAR(255) NULL,
`drop_down1` VARCHAR(255) NULL,
`drop_down2` VARCHAR(255) NULL,
`bool1` BIT NULL,
`bool2` BIT NULL,
PRIMARY KEY (`responseid`))';
I know I can just try to sanitize the $_POST['surveyid'] (like I did above) but I prefer to prepare it if possible.
$table_name = 'survey_'.$_POST['surveyid'];
Do not do the above. It is easy for a hacker to exploit your site if you include $_GET or $_POST data directly in any SQL string.
But you can't use parameters for a table name. A parameter takes the place of a single scalar value only. You can prepare CREATE TABLE but you can't use parameters for identifiers (e.g. table names).
The best practice is to make sure your table name conforms to a rule, for example only the leading portion of a string of numeric digits, up to the maximum length of a MySQL table name:
$table_name = 'survey_' . strspn(trim($_POST['surveyid']), '0123456789', 0, 56);
If you have other rules for a surveyid, then you could use preg_replace():
$table_name = 'survey_' . preg_replace('^(\w+)', '$1', trim($_POST['surveyid']));
It is not possible to prepare a data definition language statement like "CREATE TABLE". I can't find the reference in the MySQL docs that explains this, but I did find a good explanation on the PHP documentation site.

How to select specifc entries in MySQL/PHP

I'm dealing with this problem. There is tableorders(oid,datetime,quantity,title,username,mid).
The table orders is updated from php code as far as the features oid,datetime,quantity,title,username are concerned. The problem is that I want to classify each entry based on both datetime and username so as to gather these entries under an order code in order to make an ordering entry. (I can't think of anything else at the moment).
The question is how can I select those entries that are corresponding to the same username and the same date time.
For example the if I have 3entries (freddo espresso,latte,freddoccino) belong to the same order procedure (are posted by the same username, tha exact same datetime) and I need to present them to my user as a completed order.
Here is the structure of table orders:
CREATE TABLE IF NOT EXISTS `orders` (
`oid` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`datetime` DATETIME NOT NULL,
`quantity` INT NOT NULL,
`sum` FLOAT(4,2) NOT NULL,
`title` VARCHAR(30) COLLATE utf8_unicode_ci NOT NULL,
`username` VARCHAR(30) COLLATE utf8_unicode_ci NOT NULL,
`mid` VARCHAR(30) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`oid`),
KEY `username`(`username`,`mid`,`title`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=10000;
The feature title is foreign key from table products:
CREATE TABLE IF NOT EXISTS `products`(
`title` VARCHAR(30) COLLATE utf8_unicode_ci NOT NULL,
`descr` TEXT(255),
`price` FLOAT(4,2) NOT NULL,
`popularity` INT NOT NULL,
`cname` VARCHAR(20) COLLATE utf8_unicode_ci NOT NULL,
`mid` VARCHAR(30) COLLATE utf8_unicode_ci NOT NULL ,
PRIMARY KEY(`title`),
KEY `cname` (`cname`, `mid`)
)ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=10000;
Sorry If I'm a little uncomprehensive, though I really need some help to come to a conclusion. Any ideas?
Thanks in advance!
If you know what the datetime value and the username values are then you can simply use:
SELECT * FROM orders WHERE username = '$username' AND datetime = '$datetime'
However, what you would be better off doing is splitting this into two separate tables; something like:
Orders
OrderID
OrderTime
UserName
Items
ItemID
OrderID
Title
Then you would search in the following way:
SELECT Orders.OrderID, Orders.UserName, Items.Title
FROM Orders
INNER JOIN Items ON Orders.OrderID = Items.OrderID
WHERE
Orders.UserName = '$username'
AND
Orders.OrderDate = '$datetime'
When adding orders you add a record to Orders first, and then use that OrderID and add it to each item inserted in Items...
Insert Example
$mysqli; //Assuming your connection to the database...
$items; //Assuming an array of items for the order like: array('Coffee', 'Tea')
$username; //Assuming the user name to be inserted for the order
$mysqli->query("INSERT INTO Orders(`OrderTime`, `UserName`) VALUES(NOW(), '$username')");
$orderid = $mysqli->insert_id;
foreach($items as $item){
$mysqli->query("INSERT INTO Items (`OrderID`, `Title`) VALUES($orderid, '$title')");
}
NOTE: You should make sure to sanitize data before inserting to database...
Storing JSON
Storing JSON in a database is going to require you to make sure that you use a field data type that is an appropriate length (e.g. a blob).
You mentioned that you retrieve the titles as an array from a form so I'm now going to refer to that as $titles.
Saving to database
$username = '...'; // Username or id to store in database with order
$titles = array(.....); // Array of titles from form
$encodedTitles = json_encode($titles); // Convert to JSON
$mysqli->query("INSERT INTO table_name (titles_field, username_field, date_field) VALUES ('$titles', '$username', NOW())"); // Save to database (assuming already open connection
Retrieve from database
$result = $mysqli->query("SELECT titles FROM table_name WHERE username = 'username_value' AND date_field = 'date_value'"); //Run query to get row
$row = $result->fetch_assoc(); // Fetch row
$titles = json_decode($row['titles']); // This is the same as the `titles` array from the from above!
SELECT quantity,title
FROM orders
WHERE username = ? and datetime = ?
Would return the quantity of items for a specific user on specific date. Instead of a date you could use an order id, which might be a bit safer. If you use order id, then username becomes irrelevant as well, since order ids should be unique.
The answer posted with the query will help you but you should also consider changing your table structure. Looks like you could have a table named orders and another one named orders_items. Then you could list all the itens from orders_itens matching a single order.
I think this query will return kind of data where you have the same unique_id string for records where username and datetime are the same.
SELECT MD5(a.unique_id), b.* FROM (
SELECT
GROUP_CONCAT(oid) unique_id, `datetime`, username
FROM `orders` GROUP BY username, `datetime`
) a
RIGHT JOIN `orders` b
ON a.`datetime` = b.`datetime` AND a.username = b.username
ORDER BY unique_id, oid;
I also have another answer for about 3 thousands characters long but I think this variant will help you more than my long tutorial how to split the table to two tables and how to migrate data into it + php code samples. So I decided not publicate it. )))
Edit: I think you even can run this one query which is easiest and works faster:
SELECT *, MD5( CONCAT( `username` , `datetime` ) ) unique_id
FROM `orders`
ORDER BY unique_id, oid;

How can I avoid creating duplicate rows?

Everything I have searched for and found has yet to work because I am accessing the Table through a php script and differently than everything I see. Anyways,
I am importing Feeds from a website into a mysql table. My table was created like this...
$query2 = <<<EOQ
CREATE TABLE IF NOT EXISTS `Entries` (
`feed_id` int(11) NOT NULL,
`item_title` varchar(200) COLLATE utf8_unicode_ci NOT NULL,
`item_link` varchar(200) COLLATE utf8_unicode_ci NOT NULL,
`item_date` varchar(40) COLLATE utf8_unicode_ci NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
EOQ;
$result = $db_obj->query($query2);
I enter the data like so....
foreach($rss->channel->item as $Item){
$query5 = <<<EOQ
INSERT INTO Entries (feed_id, item_title, item_link, item_date)
VALUES ('$get_id','$Item->title','$Item->link','$Item->pubDate')
EOQ;
$result = $db_obj->query($query5);
}
Now, every time Import new feeds from the site I want to make sure I delete any duplicates that might already be there. Everything I have tried, especially DISTINCT, has not worked for me. Does anyone know what type of query I could use to create a temp table, copy over any distinct rows (ENTIRE ROWS, if a title is the same but the date is different I want to keep that), drop the old table, then rename the tamp table to what I want.... or something similar?
Avoid using the duplicate rows in the first place. Make any unique values into keys. When adding new values to your database, use
REPLACE INTO Entries (feed_id, item_title, item_link, item_date)
VALUES ('$get_id','$Item->title','$Item->link','$Item->pubDate')
EOQ;
The duplicates will be automatically overwritten. Replace is handy because it works like an insert when there is no conflict in the keys, but when there is then it will update the record and bump up any auto-incrementing keys.
EDIT
I've been drumming over this for a while. Here's what I came up with.
The problem with making a multi-column key on (feed_id, item_title, item_link, item_date) is that it will exceed the 1000 byte limitation in MySQL for key length. So instead alter your schema like so:
CREATE TABLE IF NOT EXISTS `Entries` (
`hash` varchar(32),
`feed_id` int(11) NOT NULL,
`item_title` varchar(200) COLLATE utf8_unicode_ci NOT NULL,
`item_link` varchar(200) COLLATE utf8_unicode_ci NOT NULL,
`item_date` varchar(40) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (hash)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
Now when you store a new value, get a hash of the values together:
$hash = md5($get_id . $Item->title . $Item->link . $Item->pubDate);
And for your insert statements use the following:
REPLACE INTO Entries (hash, feed_id, item_title, item_link, item_date)
VALUES ('$hash', '$get_id','$Item->title','$Item->link','$Item->pubDate')
EOQ;
The hash will be a unique representation of the record in it's entirety, and will be easy to compare in order to avoid duplicates. Now when you attempt to add the same record more than once, it will just replace the existing entry, and your query will not fail. As an alternative, you could continue to use insert, and the query will return an error, which you could handle however you want to.
The fastest and easiest way to delete duplicate records is by issuing a very simple command.
ALTER IGNORE TABLE [TABLENAME] ADD UNIQUE INDEX UNIQUE_INDEX ([FIELDNAME])
What this does is create a unique index on the field that you do not want to have any duplicates. The ignore syntax instructs MySQL to not stop and display an error when it hits a duplicate. This is much easier than dumping and reloading a table. It will also add unique indexes so that no new duplicates will be added. Just change you INSERT to INSERT IGNORE.
This also will work, but is not as elegant:
delete from [tablename] where fieldname in (select a.[fieldname] from
(select [fieldname] from [tablename] group by [fieldname] having count(*) > 1 ) a )
Perhaps do something like this:
$query2 = 'CREATE TABLE entries_new LIKE entries';
$result = $db_obj->query($query2);
$query5 = 'INSERT INTO entries_new (feed_id, item_title, item_link, item_date) VALUES ';
foreach($rss->channel->item as $Item){
$query5 .= '('$get_id','$Item->title','$Item->link','$Item->pubDate'),';
}
$query5 = rtrim($query5, ',');
$result = $db_obj->query($query5);
$query6 = "RENAME TABLE entries TO entries_backup, entries_new TO entries";
$result = $db_object->query($query6);
This will create a table called entries_new like your entries table. Make a single insert of data into entries_new and then rename the old table to entries_backup and the new table to entries.
You might also want to consider wrapping this whole sequence up in a transaction.

Unknown column 'login' in 'where clause'

Yeah, I know, its common problem, but I cant solve it for last 3 hours.
I do this query:
$query = 'UPDATE `'.$masterapp_users_table.'` SET `login` = "'.htmlspecialchars(strip_tags($_POST['login'])).'", `pass` = "'.md5(htmlspecialchars(strip_tags($_POST['pass']))).'", `email` = "'.htmlspecialchars(strip_tags($_POST['email'])).'", `firstname` = "'.htmlspecialchars(strip_tags($_POST['fn'])).'", `secondname` = "'.htmlspecialchars(strip_tags($_POST['sn'])).'" WHERE `login` = "'.htmlspecialchars(strip_tags($_POST['previouslogin'])).'"';
echo $query.'<br />';
mysql_query($query) or die(mysql_error());
and I get this:
UPDATE `masterapp_users` SET `login` = "asdasdasd", `pass` = "a3dcb4d229de6fde0db5686dee47145d", `email` = "asdasdasd", `firstname`
= "asdasdasd", `secondname` = "asdasdasd" WHERE `login` = "88888881"<br />Unknown column 'login' in 'where clause'
But it changes the record!
Maybe someone can see what I cant see?
Oh! forgot to say: If I paste that string from browser to PMA, it works fine.
UPDATE:
CREATE TABLE IF NOT EXISTS `masterapp_users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`login` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
`pass` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
`email` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
`firstname` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
`secondname` varchar(64) COLLATE utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `email` (`email`),
UNIQUE KEY `login` (`login`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=33 ;
The error means that the column in the MasterApp_Users table is not called login; it is Login or LOGIN or log_in or user or username or user_name or ...
In the statement that constructs the string, you have back-ticks:
UPDATE `'.$masterapp_users_table.'` SET `login` ...
In the echo, those back-ticks aren't showing.
If you use back-ticks like that, the column names become case-sensitive. What was the spelling of the CREATE TABLE statement precisely? Were column names written in mixed case inside back-ticks?
...Now we have the table schema shown, it is less explicable (not that it was easily explained before)...
Are you sure your browser and your PHP are connecting to the same database?
To debug further, I suggest changing:
The WHERE criterion to specify the id column and an appropriate value.
Do not SET the login column.
Check whether the UPDATE changes what you expect it to change.
If it doesn't change the record you think it should (but it does work), you have an issue identifying the database.
If you end up with a different column not being found, we can be pretty sure that there's a different table altogether. Maybe do a SELECT * FROM masterapp_users and review the column definitions returned.
If it changes the record, then we have a deep mystery on our hands.
It changes the record.
The complaint was specifically about the login column in the WHERE clause. If you specify id in the WHERE clause, are you able to set login in the SET clause of the statement?
If so, this is beginning to look like a bug in the DBMS. It is difficult to see why it could complain about login in WHERE and not in SET. Thus, it is unlikely to be the solution.
If the message changes to something roughly equivalent to 'unknown column login in the SET clause', then there is some self-consistency.
What happens if you rename the column in the table and modify the code accordingly?
Resolution
Comment N:
If it allows SET login = ... and not WHERE login = ... in a single statement, then I think you've got a bug. I'm surprised; it isn't like a DBMS (any DBMS) to be quite that capricious, so I'd need to be very sure about it before calling it a bug. It may also mean there's another factor at play here. If you add an echo "Hi" after the mysql_query() or die line, does that get through? Are you in fact debugging the wrong bit of SQL? Maybe there's a SELECT or something afterwards that's malformed?
Comment N+1:
Yeah thanks! After I add echo 'Hi'; after mysql_query, it appeared, so the problem was in my next queries. I knew that the problem was stupid. facepalm
Who's never made a similar mistake?
If my query works in phpMyAdmin but not from the code, the first thing I do is change
SELECT `column` FROM `table`
to
SELECT `column` FROM `database`.`table`
, or similarly with UPDATE queries of course. Perhaps this is your fix, and the MySQL error is just a bit cryptic.
Edit:
Furthermore, do not use htmlspecialchars nor strip_tags for your query escaping! It is insecure because this is not intended usage. To escape query values, it's better to use mysql_real_escape_string. Use the correct escape function for the correct application. I would write your query as follows:
$query = '
UPDATE `' . $masterapp_users_table . '`
SET `login` = "' . mysql_real_escape_string($_POST['login']) . '",
`pass` = "' . md5($_POST['pass']) . '",
`email` = "' . mysql_real_escape_string($_POST['email']) . '",
`firstname` = "' . mysql_real_escape_string($_POST['fn']) . '",
`secondname` = "' . mysql_real_escape_string($_POST['sn']) . '"
WHERE `login` = "' . mysql_real_escape_string($_POST['previouslogin']) . '"
';
// To display it in your browser:
echo htmlspecialchars($query) . '<br />';
// To run it:
mysql_query($query) or die(mysql_error());
This is just a friendly lesson. Wrong escape functions can lead to serious security holes.

Categories