MySQLi prepared statements are silentely mangling my data. Is this expected behaviour? - php

My code currently uses mysqli::query and checks mysqli::$warning_count. When trying to insert text into an integer column, 0 gets inserted and a warning is generated. However, I'm just starting to learn prepared statements and for the same query, no warning is generated.
Here's an excerpt from my code:
$db_err_msg = 'Database Error: Failed to update profile';
$sql = "UPDATE tblProfiles SET intBookId = ? WHERE lngProfileId = ?";
$stmt = $mysqli->prepare($sql) or output_error($db_err_msg);
$book = 'CA';
$id = 10773;
$stmt->bind_param('ii', $book, $id) or output_error($db_err_msg);
$stmt->execute() or output_error($db_err_msg);
echo '$stmt->affected_rows is ', $stmt->affected_rows, "\n";
if ($mysqli->warning_count) {
$warnings = $stmt->get_warnings();
do {
trigger_error('Database Warning (' . $warnings->errno . '): '
. $warnings->message, E_USER_WARNING);
} while ( $warnings->next() );
}
else {
echo 'no warnings', "\n";
}
which produces the following output:
$stmt->affected_rows is 1
no warnings
Note that the intBookId column has a TINYINT data type. The same query generates a warning when using mysqli::query, but not when using prepared statements.
Enabling strict mode does not help. It will turn the warning into an error when using mysqli::query, but when using prepared statements the column is silently updated with a 0;
For the record, my application already does extensive validation before it gets to this point. But I wanted this extra validation as way to catch anything I might miss by mistake.
Note: It's beyond the scope of the current project to switch to PDO.
Why is this happening with MySQLi prepared statements? Is this expected behaviour?

Though it may not be immediately obvious, it is expected behaviour.
Because of this line of code:
$stmt->bind_param('ii', $book, $id) or output_error($db_err_msg);
PHP is casting the value to an integer before sending it to MySQL. MySQL receives the value 0 which is a valid value for the column and no warning is generated.
If you don't want PHP to do this, then you need to send it as a string. Like so:
$stmt->bind_param('si', $book, $id) or output_error($db_err_msg);
MySQL will receive the string 'CA' and generate a warning about it being an incorrect integer value.
Note: By sending everything as a string, MySQL will have to do a little more processing to convert the strings to integers, but it was already doing that anyway when the whole query was sent as a string using mysqli::query.
Note: A related problem occurs when using integers that are greater than PHP_INT_MAX. If you think an integer value will surpass that maximum (which is platform-dependent and only 2147483647 on 32-bit platforms) and precision is important, it's safer to send it as a string.

Related

Specific MySQL PDO datetime insert [HY0093] error

I have spent a lot of time trying to figure out what is wrong here and I am stumped.
Here's the specific PHP code that is failing:
//Handy function I use to do all my bound queries - yes you can have it.
function prepareBindAndExecute($pdo, $qry, $aParams) {
if (!$stmt = $pdo->prepare($qry)) {
setSessionError("Failed to prepare $qry");
return false;
}
foreach ($aParams as $aParam) {
// $aParam[0] = ":labelToBind"
// $aParam[1] = value to Bind
// $aParam[2] = PDO::PARAM_TYPE
if (strpos($qry, $aParam[0]) !== false) { // skip binding if label isn't in query. This allows built up queries to not fail if parts were not created for a parameter.
if (!$stmt->bindParam($aParam[0], $aParam[1], $aParam[2])) {
setSessionError("Failed to bind $aParam[1] as $aParam[0] to $qry Error Info:".print_r($stmt->errorInfo()));
return false;
}
}
}
if (!$stmt->execute()) {
setSessionError("Failed to execute $qry bound with ".json_encode($aParams).' Error Info:'.print_r($stmt->errorInfo()));
return false;
}
return $stmt;
}
// Here's the problem call: The member_login is a VARCHAR(32) receiving an email address string
// and the submission_date is a DateTime column receiving the current date.
$stmt = prepareBindAndExecute($pdoRW,
'INSERT INTO videosubmissions (member_login, submission_date) VALUES (:login, :submission-date)',
[ [ ':login', $info['login'], PDO::PARAM_STR ],
[ ':submission-date', $submission_date->format(DateTime::ISO8601), PDO::PARAM_STR ] ]);
Here's the results I get with this code:
Failed to execute
INSERT INTO videosubmissions (member_login, submission_date) VALUES
(:login, :submission-date) bound with
[[":login","xTst2#gmail.com",2],
[":submission-date","2014-02-15T20:37:01+0100",2]]
With a related PHP error in the error log of:
PHP Warning: PDOStatement::execute(): SQLSTATE[HY093]: Invalid parameter
number: parameter was not defined in...
This simple case is NOT a mismatched number of parameters case as there are only two labels to bind. My helper function has been working with much more complex queries than this one.
For awhile I thought I had this fixed by quoting the :label tag -> VALUES (":label", :submission_date...
This let the call succeed but resulted in the sting ":label" being inserted into the DB and would in fact, by my understanding, cause a true parameter count mismatch.
The PDO::PARAM_ constants do not offer a DATE or DATETIME flavor. (see http://www.php.net/manual/en/pdo.constants.php)
I have verified that my helper function did not skip binding any parameters - and we can seed this from the error message returned.
I have also tried binding the submission_date with a DateTime PHP object instead of a string and I have tried various data/time formated strings.
I am wondering if the # in the login parameter is somehow screwing up the binding.
It would be nice if PDO would offer up what the actual query is that is being sent to the mySql but this might be buried in the driver.
Hopefully I am just missing something dumb.
Thanks!!!
Dunno what's the problem with this particular code, but if to take out all the useless parts it can be boiled down to this, and I am pretty sure would work
function query($pdo, $qry, $aParams) {
$stmt = $pdo->prepare($qry);
$stmt->execute($aParams);
return $stmt;
}
$sql = 'INSERT INTO videosubmissions (member_login, submission_date) VALUES (?, ?)';
query($pdoRW, $sql, [$info['login'], $submission_date->format(DateTime::ISO8601)]);
You can't have - in a placeholder name:
INSERT INTO [...snip...] (:login, :submission-date)',
^---
The - is not a valid char in a placeholder name, and MySQL will interpreter this as
... :submission MINUS date
Since you're not binding a value for submission, you get the invalid parameter number error. Even if you by-chance did have another placeholder whose name WAS :submission, you'd still end up with an SQL parser error due to the undefined/non-existent date field being used in what MySQL is seing as a subtraction operation.

What's the difference between mysqli::$affected_rows and mysqli_stmt::$affected_rows?

Obviously, mysqli_stmt::$affected_rows is not available unless prepared statements are being used. But when prepared statements are being used, what's the difference between mysqli::$affected_rows and mysqli_stmt::$affected_rows?
I have the same question in regards to mysqli::$insert_id vs mysqli_stmt::$insert_id.
I'm trying to decide if I should be using one in favour of the other.
I've read the PHP manual entries for these properties. I've done some testing (PHP 5.3.17) using a single execute and using multiple executes. I don't see a difference.
So I am wondering if maybe there is some difference under certain circumstances (or certain versions). If they are exactly the same, why have both?
mysqli_stmt::$affected_rows:
Returns the total number of rows changed, deleted, or inserted by the
last executed statement
mysqli::$affected_rows:
Gets the number of affected rows in a previous MySQL operation
So, if the mysqli_stmt object was the last executed statement, both queries should give the same result.
I think the only reason to keep mysqli:$affected_rows is mysqli::query and mysqli::multi_query, because they both don't use prepared statements. And the only reason to keep mysqli_stmt:$affected_rows is OOP: to encapsulate query related information in statement object.
I just discovered a difference between mysqli_stmt::$affected_rows and mysqli::$affected_rows that I didn't expect.
I assumed that mysqli::$affected_rows could be called after closing the statement because I expected it to report based on the last query executed on the connection. I didn't think it would matter if the statement was closed. However, it does seem to make a difference.
This code:
$db_err_msg = 'Database Error: Failed to update profile';
$sql = "UPDATE tblProfiles SET lngPhoneNumber = ? WHERE lngProfileId = ?";
$stmt = $mysqli->prepare($sql) or output_error($db_err_msg);
$phone = 5555555555;
$id = 10773;
$stmt->bind_param('ii', $phone, $id) or output_error($db_err_msg);
$stmt->execute() or output_error($db_err_msg);
if ($mysqli->warning_count) {
$warnings = $mysqli->get_warnings();
do {
trigger_error('Database Warning (' . $warnings->errno . '): '
. $warnings->message, E_USER_WARNING);
} while ( $warnings->next() );
}
else {
echo 'no warnings', "\n\n";
}
echo 'Before $stmt->close()', "\n";
echo '$mysqli->affected_rows is ', $mysqli->affected_rows, "\n";
echo '$stmt->affected_rows is ', $stmt->affected_rows, "\n";
echo '$mysqli->affected_rows is ', $mysqli->affected_rows, "\n";
$stmt->close();
echo "\n", 'After $stmt->close()', "\n";
echo '$mysqli->affected_rows is ', $mysqli->affected_rows, "\n";
produces this output:
no warnings
Before $stmt->close()
$mysqli->affected_rows is 1
$stmt->affected_rows is 1
$mysqli->affected_rows is 1
After $stmt->close()
$mysqli->affected_rows is -1
Note how the final value is negative 1.
The PHP manual for mysqli::$affected_rows says:
-1 indicates that the query returned an error
The query updated the record as expected and did not return an error or warning. Yet this implies there was an error. I'm not sure if this is a bug or not, but it was certainly not what I expected. Regardless of which one you use, clearly the safest approach is to check it right after the execute statement.

PDO Prepared Statement over ODBC Sybase "PARAM datastream" error

I am trying to convert some old PHP ODBC queries over to PDO Prepared statements and am getting an error I cannot find too much information on.
The Error is:
"[DataDirect][ODBC Sybase Wire Protocol driver][SQL Server]There is no host variable corresponding to the one specified by the PARAM datastream. This means that this variable '' was not used in the preceding DECLARE CURSOR or SQL command. (SQLExecute[3801] at ext\pdo_odbc\odbc_stmt.c:254)"
I am searching for a single row in the database using a 6 digit ID that is stored in the database as a VARCHAR but is usually a 6 digit number.
The database connection is reporting successful.
The ID passed by the query string is validated.
The prepared statement results in the above error.
The backup straight ODBC_EXEC statement in the else clause returns the data I am looking for.
//PDO Driver Connect to Sybase
try {
$pdo = new PDO("odbc:Driver={Sybase ASE ODBC Driver};NA=server,5000;Uid=username;Pwd=password;");
$pdo_status = "Sybase Connected";
} catch(PDOException $e) {
echo 'Connection failed: ' . $e->getMessage();
}
if((isset($_GET['id'])) AND ($_GET['id'] != "")) {
//Validate ID String
if(!preg_match("/^[A-Za-z0-9]{5,7}/",$_GET['id'])) {
$query1_id = FALSE;
echo "Invalid ID";
exit;
} else {
$query1_id = $_GET['id'];
}
$query1 = $pdo->prepare("SELECT * FROM People WHERE PersonId= ?");
$query1->execute(array($query1_id));
if($query1->errorCode() != 0) {
$person_data = $query1->fetch(PDO::FETCH_ASSOC);
echo "Person Data from PDO: ";
print_r($person_data);
} else {
$errors = $query1->errorInfo();
echo $errors[2];
//Try the old way to confirm data is there.
$odbc_query1 = "SELECT * FROM People WHERE PersonId='$query1_id' ";
$person_result = odbc_exec($conn,$odbc_query1) or die("Error getting Data, Query 1");
$person_data = odbc_fetch_array($person_result);
echo "Person Data from ODBC_EXEC: ";
print_r($person_data);
}
It also fails if I use:
$query1 = $pdo->prepare("SELECT * FROM People WHERE PersonId= :id ");
$query1->execute(array(":id"=>$query1_id));
Does anyone have experience with this error?
Edit: Sybase Manual says this about the error...
Error 3801: There is no host variable corresponding to the one specified by the PARAM datastream. This means that this variable `%.*s' was not used in the preceding DECLARE CURSOR or SQL command.
Explanation:
Adaptive Server could not perform the requested action. Check your command for missing or incorrect database objects, variable names, and/or input data.
Which is odd because my error (quoted at the top) doesn't tell me which variable has no host.
Also fails if I use...
$query1 = $pdo->prepare("SELECT * FROM People WHERE PersonId= :id ");
$query1->bindParam(':id',$query1_id,PDO::PARAM_STR); //Or PARAM_INT
$query1->execute();
The query works if I place the variable in the query like this...
$query1 = $pdo->prepare("SELECT * FROM People WHERE PersonId= '$query1_id'");
So I think it has something to do with the parameter not being bound to the placeholder but I can't figure out why.
If I can't work this out I'll have to revert to building my query as a string and hoping my input validation is bullet proof.
Your problem seems to be with the default data type PHP assigns to variables in the placeholders. The SQL Statement is looking for a number but PHP is interpreting it as something else. You can prevent this using quotes around the placeholder variable. Notice that in the statements that work you have apostrophes ('') around the value that PHP sees:
$query1 = $pdo->prepare("SELECT * FROM People WHERE PersonId= '$query1_id'");
Try this when using the placeholder it should be the same:
$query1 = $pdo->prepare("SELECT * FROM People WHERE PersonId= ':id'");

Calling prepare with mysqli won't fetch data

$data = $mysqli->prepare("SELECT amount FROM items WHERE id=:id");
echo 'forward1';
if(!$data->execute(array(':id' => $id)))
die("error executing".$data->error);
echo '2';
$row = $data->fetch_object();
die('Losing my mind'.$row->amount);
This will only echo "forward1", not "error executing..." or "2". It works with *$mysqli->query". If I add quotes '' to :id in the query, it will echo "forward1error executing".
First, make sure you understand the prepared statements syntax and working model.
As in:
$data = $mysqli->prepare("SELECT amount FROM items WHERE id=(?)");
// THIS ^^ actually "prepares" an object to be used in the statement
$data->bind_param("i",$id)
// ...then you "bind" the parameter for your statement as "i"(nteger)
echo 'forward1';
if(!$data->execute()) // And now you simply run it, with no other args
die("error executing".$data->error);
echo '2';
$row = $data->fetch_object();
die('Loosing my mind'.$row->amount);
I suggest though using something more like
$data->execute() or die("error executing".$data->error);
The main steps of a prepared statement are:
1. Prepare the query with some placeholder values;
2. "Bind" the required number of values to the query;
3. Execute it!
I fail to see why this is relevant in your case, with such a simple query. I also assume you actually need it for something bigger.
Please let me know if I misunderstood your point or code sample.
Oh, and.. have fun! :-)
Turn on your error reporting.
You get a fatal error by accessing the method execute on your mysqli::statement after prepare failed. Check if $data === false before calling execute.
Error message: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ':id' at line 1
See this answer to why this error is triggered: MYSQLI::prepare() , error when used placeholder :something
See the PHP manual on how to use mysqli, or use PDO instead.

PHP mySql update works fine on localhost but not when live

I have a a php page which updates a mySql database it works fine on my mac (localhost using mamp)
I made a check if its the connection but it appears to be that there is a connection
<?php require_once('connection.php'); ?>
<?php
$id = $_GET['id'];
$collumn = $_GET['collumn'];
$val = $_GET['val'];
// checking if there is a connection
if(!$connection){
echo "connectioned failed";
}
?>
<?php
$sqlUpdate = 'UPDATE plProducts.allPens SET '. "{$collumn}".' = '."'{$val}'".' WHERE allPens.prodId = '."'{$id}'".' LIMIT 1';
mysql_query($sqlUpdate);
// testing for errors
if ($sqlUpdate === false) {
// Checked this and echos NO errors.
echo "Query failed: " . mysql_error();
}
if (mysql_affected_rows() == 1) {
echo "updated";
} else {
echo "failed";
}?>
In the URL i pass in parameters and it looks like this: http://pathToSite.com/updateDB.php?id=17&collumn=prodid&val=4
Maybe this has to do with the hosting? isn' t this simple PHP mySql database updating? what can be wrong here?
Why on localhost it does work?
Why on live server it doesn't?
Let's start with troubleshooting your exact problem. Your query is failing for some reason. We can find out what that problem is by checking what comes back from mysql_query, and if it's boolean false, asking mysql_error what went wrong:
$sh = mysql_query($sqlUpdate);
if($sh === false) {
echo "Query failed: " . mysql_error();
exit;
}
You have other problems here. The largest is that your code suffers from an SQL Injection vulnerability. Let's say your script is called foo.php. If I request:
foo.php?collumn=prodId = NULL --
then your SQL will come out looking like:
UPDATE plProducts.allPens SET prodId = NULL -- = "" WHERE allPens.prodId = "" LIMIT 1
-- is an SQL comment.
I just managed to nuke all of the product IDs in your table.
The most effective way to stop SQL injection is to use prepared statements and placeholders. The "mysql" extension in PHP doesn't support them, so you'd also need to switch to either the must better mysqli extension, or the PDO extension.
Let's use a PDO prepared statement to make your query safe.
// Placeholders only work for *data*. We'll need to validate
// the column name another way. A list of columns that can be
// updated is very safe.
$safe_columns = array('a', 'b', 'c', 'd');
if(!in_array($collumn, $safe_columns))
die "Invalid column";
// Those question marks are the placeholders.
$sqlUpdate = "UPDATE plProducts.allPens SET $column = ? WHERE allPens.prodId = ? LIMIT 1";
$sh = $db->prepare($sqlUpdate);
// The entries in the array you pass to execute() are substituted
// into the query, replacing the placeholders.
$success = $sh->execute(array( $val, $id ));
// If PDO is configured to use warnings instead of exceptions, this will work.
// Otherwise, you'll need to worry about handling the exception...
if(!$success)
die "Oh no, it failed! MySQL says: " . join(' ', $db->errorInfo());
Most mysql functions return FALSE if they encounter an error. You should check for error conditions and if one occurs, output the error message. That will give you a better idea of where the problem occurred and what the nature of the problem is.
It's amazing how many programmers never check for error states, despite many examples in the PHP docs.
$link = mysql_connect(...);
if ($link === false) {
die(mysql_error());
}
$selected = mysql_select_db(...);
if ($selected === false) {
die(mysql_error());
}
$result = mysql_query(...);
if ($result === false) {
die(mysql_error());
}
Your call to mysql_query() is faulty; you're checking the contents of the variable you're passing in but the function call doesn't work that way. It returns a value which is what you should check. If the query failed, it returned false. If it returns data (like from a SELECT) it returns a resource handle. If it succeeds but doesn't return data (like from an INSERT) it returns true.
You also have some problems constructing your SQL. #Charles mentions SQL injection and suggests prepared statements. If you still want to construct a query string, then you need to use mysql_real_escape_string(). (But I would recommend you read up on the mysqli extension and use those functions instead.)
Secondly, you're concatenating strings with embedded substitution. This is silly. Do it this way instead:
$sqlUpdate = 'UPDATE plProducts.allPens SET '.$collumn.' = \''.$val.'\'
WHERE allPens.prodId = '.intval($id).' LIMIT 1';
If you must accept it in the querystring, you should also check that $collumn is set to a valid value before you use it. And emit and error page if it's not. Likewise, check that $id will turn into a number (use is_numeric()). All this is called defensive programming.

Categories