I have two issues, the first as the title states is that I need to have dynamic query with AND/OR in it. I fully understand the AND part (I've done a bunch of these) however, the OR part is very confusing to me because looking at this following sql :
$sql = SELECT * FROM table WHERE 1
then if you add an OR statement if a condition is met :
if(isset($_POST['OR'])){
$sql. = " OR peaches = :good";
}
then the query will return WHERE 1 OR peaches = :good
Again I understand the part with the AND, but I do not understand how to set up the OR part.
This is how I have set up the AND / OR selection (and this works)
The second issue I am facing is this code snippet from the same script (please read code comments) :
$sql .= " GROUP BY anum"; // I always group BY anum no matter what
if ($count !== "") { // if COUNT is not ""
$sql .= " HAVING COUNT(session.anum) :count"; // Then I want the user to be able to choose the operator (> < => =< =) and the dynamic number for it to use
$placeholder[':count'] = $count; // Then add the key :count to an array with the value of $count
}
$dynamic = $this->db->conn_id->prepare($sql);
$dynamic->execute($placeholder);
So as you notice I give the named parameter (:count) the value of $count, however this does "not work".
Is it possible to do what I am trying to do ($sql .= " HAVING COUNT(session.anum) :count";)
If not then I could just do : $sql .= " HAVING COUNT(session.anum) $count";
but that would defeat the purpose of PDO.
Any help would be great
Problem 1:
The reason that some developers use WHERE 1 when they have optional search terms is that an expression like TRUE AND <condition> is always equal to <condition>. This is basic boolean algebra.
But this is not the case for OR expressions. TRUE OR <condition> is always simply TRUE. You could modify your base query to use WHERE 0 so that when you append an OR term it comes out as WHERE 0 OR <condition>. Any expression like FALSE OR <condition> is always equal to the <condition>.
If you need to support both AND and OR in the same SQL query, you need to start putting parentheses around terms so they evaluate in the way you intend. I'm not going to explain boolean algebra and MySQL's operator precedence in this StackOverflow answer. But suffice to say that simply appending terms with .= isn't going to work when you have a mix of AND and OR terms.
Problem 2:
Parameters are very useful, but they don't solve every case of dynamic SQL. You can use an SQL parameter in place of a single literal value, but nothing else.
Not table names
Not column names
Not lists of values (like an IN( ) predicate)
Not SQL keywords
Not expressions
Not operators
You have to use string interpolation to include a user-chosen operator in your HAVING clause.
It's recommended to use whitelisting to avoid risk of SQL injection when you need to interpolate dynamic content and can't use a parameter.
For the first issue, what is the problem exactly?
For the second, MySQL manual says that you can't use functions on having clauses.
You can do like this:
SELECT *, COUNT(session.anum) AS total GROUP BY session.anum HAVING total > :count
Related
a quick question :), I wrote this because someone said that my codes are vulnerable to mysql injection and it is a requirement to learn prepared statement in web programming to avoid any user putting malicious data or statement into the database..What I have is a search function that search data from the database, if you type in a string like this "torres" then i search for torres but if you just put "tor" it won't search for datas that contain "tor" in their name..I don't know the correct format while using prepared statement, If you have advice I'm very happy to take it :)
<?php
if (isset($_POST['search'])) {
$box = $_POST['box'];
$box = preg_replace("#[^0-9a-z]#i","",$box);
$grade =$_POST['grade'];
$section = $_POST['section'];
$strand = $_POST['strand'];
$sql = "SELECT * FROM student WHERE fname LIKE ? or lname LIKE ? or mname LIKE ? or grade = ? or track = ? or section = ?";
$stmt = mysqli_stmt_init($conn);
if (!mysqli_stmt_prepare($stmt, $sql)){
echo "SQL FAILED";
}
else {
//bind the parameter place holder
mysqli_stmt_bind_param($stmt, "ssssss",$box, $box, $box, $grade, $strand, $section);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
while($row = mysqli_fetch_assoc($result))
{
echo "<tr>";
echo "<td>".$row['lname']."</td>";
echo "<td>".$row['fname']."</td>";
echo "<td>".$row['mname']."</td>";
echo "<td>".$row['grade']."</td>";
echo "<td>".$row['track']."</td>";
echo "<td>".$row['section']."</td>";
echo "</tr>";
}
}
As requested:
#ArtisticPhoenix I clearly prefer the king's way [compound full text index]. This should be your primary answer showing an example/explaination.
First make a full text index that includes all three fields (this is in PHPmyAdmin, it's a bit easier to explain with an image)
Then do a query like this:
#PDO version SELECT * FROM `temp` WHERE MATCH(fname,mname,lname)AGAINST(:fullname IN BOOLEAN MODE)
#MySqli version SELECT * FROM `temp` WHERE MATCH(fname,mname,lname)AGAINST(? IN BOOLEAN MODE)
SELECT * FROM `temp` WHERE MATCH(fname,mname,lname)AGAINST('edward' IN BOOLEAN MODE)
It seems simple but there are some things with full text to be aware of Min char count which is 3 (I think) anything smaller than that is not searched on. This can be changed but it requires repairing the DB and restarting MySql.
Stop words, these are things like and, the etc. These can also be configured in my.cnf.
Punctuation is ignored. This might not seem a big deal on names but think of hyphenated last names.
Usually I reduce the word min to 2 and point the stopwords to an empty file (disabling them).
The match against syntax is quite different, it's pretty powerful but it's not really used outside of full text. An example is: this is the wild card * and you use '"' double quotes for exact phrase match '"match exactly"', and + is logical AND, such as word+ word+ (default is or), - is do not match this etc... If I remember right, I used it a bunch a few years ago but haven't had to use it recently.
For example doing "begins with" on a partial word
SELECT * FROM `temp` WHERE MATCH(fname,mname,lname)AGAINST('edwar*' IN BOOLEAN MODE)
Same result matches one row. The obvious benefit is searching all 3 fields at the same time, but the full text syntax itself can be quite useful too.
For more information:
https://dev.mysql.com/doc/refman/8.0/en/fulltext-boolean.html
PS. I might add that using OR in a query can really kill performance, I've went as far as to replace simple OR with a UNION because of how bad the performance is on a large table. Logically the DB optimizer has to rescan the entire table for an OR, unlike AND where it can use the result of the previous expression to reduce the next expressions data set (or that is how I understand it). I can say the performance difference is very noticeable using OR vs UNION.
This is true for a compound full text index vs doing OR on each field separately. By default fulltext is faster, but it's even faster this way.
To fix your current query (for the sake of completeness)
You need whats known as an exclusive or, like this:
SELECT * FROM student WHERE ( fname LIKE ? OR lname LIKE ? OR mname LIKE ? ) AND grade = ? AND track = ? AND section = ?
What this does is group the OR's together so that they evalute as one expression to the "next level up" ( outside the parenthesis ). Basically order of operations. In English, you would have to match at least 1 of these columns fname, lname, mname AND you would also have to match all of the rest of the columns as well, to get a result returned for any given row.
If you use all OR (as you are now) and any single field matches, then the query comes back as true with matches. Which is the behaviour you are experiencing now.
If you simply change everything outside of the name fields to AND, Basically remove the parenthesis
Like this:
#this is wrong don't use it.
SELECT * FROM student WHERE fname LIKE ? OR lname LIKE ? OR mname LIKE ? AND grade = ? AND track = ? AND section = ?
Then you have to match this way.
(grade AND track AND section AND mname) OR lname OR fname
So if the last or first name match you get results regardless of any of the other fields. But the mname field you would find has to match with all the rest of the fields to get a result (but you would not likely notice this). Because, it would seem that the query works how you want but only when the mname is a match.
I hope that makes sense. It may be helpful to think of the WHERE clause as an IF condition the same logic rules apply.
Cheers!
A mysqli_stmt does not have a query_params() function, I had to write my own. The parameter arry is bound to the statement with bind_param(). I need to specify the variable types dynamically. I could do that with something like:
$sType = '';
foreach ($aParameters as $iIndex => $mParameter) {
if (is_string($mParameter)) {$sType .= 's';}
elseif ( is_int($mParameter)) {$sType .= 'i';}
elseif ( is_float($mParameter)) {$sType .= 'd';}
elseif ( is_null($mParameter)) {$sType .= 's';}
elseif ( is_bool($mParameter)) {
$sType .= 'i';
$aParameters[$iIndex] = boolval($mParameter);}
else {
// trow new Exception(...);
}
}
But as it turns out, mysql/mariadb will send booleans, integers and floats fine as strings, where the database server will happily cast them to the corresponding data type of the column. It seems like I could just skip this step and send every parameter as a string by default.
Are there any reaons to specify another data type than "s" for each parameter?
EDIT: I just found this SO topic which shows how to use the "b" type and mysqli_stmt::send_long_data when the binary packet would exceed the max_allowed_packet setting. I also have read that it will improve performance over solutions that employ bin2hex() to turn send a byte string as text.
It seems like I could just skip this step and send every parameter as a string by default.
Yes, exactly.
Are there any reasons to specify another data type than "s" for each parameter?
Extremely rare and vague. So far I was able to find as much as
bigint values are better to be bound as integers rather than strings
some report that casting could cause the wrong execution plan but I was unable to find a proof in the wild
the binary type you already found yourself, though I would question the idea itself of storing BLOBs in the database
that odd order by number case mentioned by Dharman.
However odd the case could be, I would propose to keep the typed binding but avoid that type sniffing, which does no good but could destroy your database.
Instead, just make types explicit but optional, like I did in my mysqli helper function:
function prepared_query($mysqli, $sql, $params, $types = "")
{
$types = $types ?: str_repeat("s", count($params));
$stmt = $mysqli->prepare($sql);
$stmt->bind_param($types, ...$params);
$stmt->execute();
return $stmt;
}
when you don't need them (most of time), just leave types out:
$sql = "SELECT * FROM tmp_mysqli_helper_test LIMIT ?";
$res = prepared_query($conn, $sql, [10])->get_result();
but every time you will need it, it's already here and explicit, you could set the exact type you want:
$sql = "INSERT INTO table (id, blobfield) VALUES (?, ?)";
prepared_query($conn, $sql, [$id, $file], "ib");
Clean, simple and elegant!
The only time I have found it's important to use an integer parameter is in a LIMIT clause.
SELECT
...
LIMIT ?, ?
MySQL does not accept quoted string literals in this context, and does not accept parameters with string type. You have to use an integer.
See Parametrized PDO query and `LIMIT` clause - not working for my tests on this. That was a question about PDO, and I didn't test mysqli, but I believe it's a server-side MySQL requirement to use integer parameters in this case. So it should apply to mysqli too.
In all other cases (AFAIK), MySQL is able to convert strings into integers by reading the leading digits in the string, and ignoring any following characters.
#Dharman in a comment below makes reference to MySQL's support for integers in ORDER BY:
SELECT
...
ORDER BY ?
An integer in ORDER BY means to sort by the column in that position, not by the constant value of the number:
SELECT
...
ORDER BY 1 -- sorts by the 1st column
But an equivalent string value containing that number doesn't act the same. It sorts by the constant value of the string, which means every row is tied, and the sort order will be arbitrary.
SELECT
...
ORDER BY '1' -- sorts by a constant value, so all rows are tied
Therefore this is another case where the data type for a query parameter is important.
On the other hand, using ordinal numbers to sort by the column in that position in ORDER BY or GROUP BY is deprecated, and we shouldn't rely on that usage of SQL.
Trying to build a searchpage for an equipment database, and I have a prepared statement which takes an equipment 'tag' (always required) and searches a keyword in six different data fields where the 'tag' matches user input.
The problem is that no results are found if the keyword is IN one of these possibly long fields, so many intended results are missing.
In regular SQL I'd just to a table join and add a LIKE query to an IN query; but with mysqli I'm using a prepared statement with the ? placeholder and not sure what can be done about using this mark more than once for each variable.
The statement I have is this:
if (!($stmt = $conn->prepare("SELECT * FROM equipment
WHERE EQUIP_CND LIKE ?
AND (EQUIP_TYP LIKE ? OR ((EQUIP_SER LIKE ? OR EQUIP_PNO LIKE ?)
OR (EQUIP_LOC LIKE ? OR EQUIP_CMT LIKE ?)));"))) {
echo "Prepare failed: (" . $conn->errno . ") " . $conn->error;
}
And I want to just replace LIKE with LIKE IN. How to achieve this?
The like requires wildcards to have loose matching.
e.g.
select * from table where a like 'b'
is the same as:
select * from table where a = 'b'
so a record of b would be found but abc would not.
From the manual:
With LIKE you can use the following two wildcard characters in the pattern:
% matches any number of characters, even zero characters.
_ matches exactly one character.
So to find abc you'd use:
select * from table where a like '%b%'
For prepared statements the wildcards get appended to the variable, or in the binding, NOT in the query itself. Example 6 on the PDO manual page shows this. http://php.net/manual/en/pdo.prepared-statements.php#example-991 (after the comment // placeholder must be used in the place of the whole value)
I'm writing a filter/sorting feature for an application right now that will have text fields above each column. As the user types in each field, requests will be sent to the back-end for sorting. Since there are going to be around 6 text fields, I was wondering if there's a better way to sort instead of using if statements to check for each variable, and writing specific queries if say all fields were entered, just one, or just two fields, etc.
Seems like there would be a lot of if statements. Is there a more intuitive way of accomplishing this?
Thanks!
Any initial data manipulation, such as sorting, is usually done by the database engine.
Put an ORDER BY clause in there, unless you have a specific reason the sorting needs done in the application itself.
Edit: You now say that you want to filter the data instead. I would still do this at the database level. There is no sense in sending a huge dataset to PHP, just for PHP to have to wade through it and filter out data there. In most cases, doing this within MySQL will be far more efficient than what you can build in PHP.
Since there are going to be around 6 text fields, I was wondering if there's a better way to sort instead of using if statements to check for each variable
Definitely NO.
First, nothing wrong in using several if's in order.
Trust me - I myself being a huge fan of reducing repetitions of code, but consider these manually written blocks being the best solution.
Next, although there can be a way to wrap these condition ns some loop, most of time different conditions require different treatment.
however, in your next statements you are wrong:
and writing specific queries
you need only one query
Seems like there would be a lot of if statements.
why? no more than number of fields you have.
here goes a complete example of custom search query building code:
$w = array();
$where = '';
if (!empty($_GET['rooms'])) $w[]="rooms='".mesc($_GET['rooms'])."'";
if (!empty($_GET['space'])) $w[]="space='".mesc($_GET['space'])."'";
if (!empty($_GET['max_price'])) $w[]="price < '".mesc($_GET['max_price'])."'";
if (count($w)) $where="WHERE ".implode(' AND ',$w);
$query="select * from table $where";
the only fields filled by the user going to the query.
the ordering is going to be pretty the same way.
mesc is an abbreviation for the mysql_real_escape_string or any other applicable database-specific string escaping function
select * from Users
order by Creadted desc, Name asc, LastName desc, Status asc
And your records will be sorted by order from query.
First by Created desc, then by Name asc and so on.
But from your question I can see that you are searching for filtering results.
So to filter by multiple fileds just append your where, or if you are using any ORM you can do it through object methods.
But if its simple you can do it this way
$query = "";
foreach($_POST['grid_fields'] as $key => $value)
{
if(strlen($query) > 0)
$query .= ' and '
$query .= sprintf(" %s LIKE '%s' ", mysql_real_escape_string($key), '%' .mysql_real_escape_string($value) .'%');
}
if(strlen($query) > 0)
$original_query .= ' where ' . $query;
this could help you to achieve your result.
No. You cannot avoid the testing operations when sorting the set, as you have to compare the elements in the set in same way. The vehicle for this is an if statement.
Could you take a look at this?
WHERE (ifnull(#filter1, 1) = 1 or columnFilter1 = #filter1)
and (ifnull(#filter2, 1) = 1 or columnFilter2 = #filter2)
and (ifnull(#filter3, 1) = 1 or columnFilter3 = #filter3)
and (ifnull(#filter4, 1) = 1 or columnFilter4 = #filter4)
and (ifnull(#filter5, 1) = 1 or columnFilter5 = #filter5)
and (ifnull(#filter6, 1) = 1 or columnFilter6 = #filter6)
Please let me know if I'm misunderstanding your question.. It's not like an IF statement batch, and is pretty lengthy, but what do you think?
I have a function that I use called sqlf(), it emulates prepared statements. For instance I can do things like:
$sql = sqlf("SELECT * FROM Users WHERE name= :1 AND email= :2",'Big "John"','bj#example.com') ;
For various reasons, I cannot use prepared statements, but I would like to emulate them. The problem that I run into is with queries like
$sql = sqlf("SELECT * FROM Users WHERE id IN (:1)",array(1,2,3) );
My code works, but it fails with empty arrays, e.g. the following throws a mysql error:
SELECT * FROM Users WHERE id IN ();
Does anyone have any suggestions? How should I translate and empty array into sql that can be injected into an IN clause? Substituting NULL will not work.
Null is the only value that you can guarantee is not in the set. How come it is not an option? Anything else can be seen as part of the potential set, they are all values.
I would say that passing an empty array as argument for an IN() clause is an error. You have control over the syntax of the query when calling this function, so you should also be responsible for the inputs. I suggest checking for emptiness of the argument before calling the function.
Is there a possibility that you could detect empty arrays withing sqlf and change the SQL to not have the IN clause?
Alteratively, you could postprocess the SQL before passing it to the "real" SQL executor so that "IN ()" sections are removed although you'd have to do all sorts of trickery to see what other elements had to be removed so that:
SELECT * FROM Users WHERE id IN ();
SELECT * FROM Users WHERE a = 7 AND id IN ();
SELECT * FROM Users WHERE id IN () OR a = 9;
would become:
SELECT * FROM Users;
SELECT * FROM Users WHERE a = 7;
SELECT * FROM Users WHERE a = 9;
That could get tricky depending on the complexity of your SQL - you'd basically need a full SQL language interpreter.
If your prepare-like function simply replaces :1 with the equivalent argument, you might try having your query contain something like (':1'), so that if :1 is empty, it resolves to (''), which will not cause a parse error (however it may cause undesirable behavior, if that field can have blank values -- although if it's an int, this isn't a problem). It's not a very clean solution, however, and you're better off detecting whether the array is empty and simply using an alternate version of the query that lacks the "IN (:1)" component. (If that's the only logic in the WHERE clause, then presumably you don't want to select everything, so you would simply not execute the query.)
I would use zero, assuming your "id" column is a pseudokey that is assigned numbers automatically.
As far as I know, automatic key generators in most brands of database begin at 1. This is a convention, not a requirement (auto-numbered fields are not defined in standard SQL). But this convention is common enough that you can probably rely on it.
Since zero probably never appears in your "id" column, you can use this value in the IN() predicate when your input array is empty, and it'll never match.
The only way I can think to do it would be to make your sqlf() function scan to see if a particular substitution comes soon after an "IN (" and then if the passed variable is an empty array, put in something which you know for certain won't be in that column: "m,znmzcb~~1", for example. It's a hack, for sure but it would work.
If you wanted to take it even further, could you change your function so that there are different types of substitutions? It looks like your function scans for a colon followed by a number. Why not add another type, like an # followed by a number, which will be smart to empty arrays (this saves you from having to scan and guess if the variable is supposed to be an array).