PDO exception on correct MySql query - php

I'm trying to update a MySql database using PDO in PHP.
I need to launch 4 lines of SQL code. The last one is the SELECT statement, which should return only one integer value.
The error thrown is:
exception 'PDOException' with message 'SQLSTATE[HY000]: General error' in C:\xampp\htdocs\php\set-lesson-finished.php:22
Stack trace:
#0 C:\xampp\htdocs\php\set-lesson-finished.php(22): PDOStatement->fetchColumn()
#1 {main}
And here is the code:
try {
$db_connection = new PDO('mysql:host='. DB_HOST .';dbname='. DB_NAME . ';charset=utf8', DB_USER, DB_PASS, array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
$query_save = $db_connection->prepare('
DELETE FROM unfinished_lessons WHERE bought_id = :bought_id;
INSERT INTO finished_lessons VALUES (:bought_id, :time_invested, NOW());
UPDATE users SET points = points + :points_earned WHERE id = :user_id;
SELECT points FROM users WHERE id = :user_id;
');
$query_save->bindValue(':user_id', (int)$_SESSION['user_id'], PDO::PARAM_INT);
$query_save->bindValue(':bought_id', (int)$_POST['bought_id'], PDO::PARAM_INT);
$query_save->bindValue(':time_invested', (int)$_POST['time_invested'], PDO::PARAM_INT);
$query_save->bindValue(':points_earned', (int)$_POST['points_earned'], PDO::PARAM_INT);
$query_save->execute();
echo $query_save->fetchColumn();
} catch (PDOException $e) {
echo $e;
}

This is not a single query but four queries together. You'll get four different result sets which need to be retrieved independently.
The error you're seeing is occurring because you can't call fetchColumn() on a DELETE query (it doesn't return a result set). Likewise for INSERT and UPDATE.
You could experiment with PDOStatement::nextRowSet(), but it's probably easier to run the four queries separately. If you're concerned about having an all-or-nothing update you should use a transaction with PDO::beginTransaction() and PDO::commit()

When queries gets complex you need to use a stored procedure:
DELIMITER //
CREATE PROCEDURE your_sp(IN `p_bought_id`, IN `p_time_invested`, IN `p_points_earned`, IN `p_user_id`)
BEGIN
DELETE FROM unfinished_lessons WHERE bought_id = p_bought_id;
INSERT INTO finished_lessons VALUES (p_bought_id, p_time_invested, NOW());
UPDATE users SET points = points + p_points_earned WHERE id = p_user_id;
SELECT points FROM users WHERE id = p_user_id;
END //
DELIMITER ;
You call it in one query like this:
$query_save = $db_connection->prepare('
call your_sp(:bought_id, :time_invested,:points_earned, :user_id)
');

You should use mysqli extension (and not PDO) if you absolutely want to run multiple statements at the same time.
As you can see in this Feature comparison (see the comparison table), only ext/mysqli has full support for multiple MySQL queries (and also supports all MySQL 5.1+ functionnality).
And, as Drupal's dawehner says, MySQL PDO no longer allows multiple database queries to be executed at the same time.

I've split the queries in four and everything works as expected:
$db_connection = new PDO('mysql:host='. DB_HOST .';dbname='. DB_NAME . ';charset=utf8', DB_USER, DB_PASS, array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));
$db_connection->beginTransaction();
$query = $db_connection->prepare('DELETE FROM planz_unfinished_lessons WHERE bought_id = :bought_id;');
$query->bindValue(':bought_id', (int)$_POST['bought_id'], PDO::PARAM_INT);
$query->execute();
$query = $db_connection->prepare('INSERT INTO planz_finished_lessons VALUES (:bought_id, :time_invested, NOW())');
$query->bindValue(':bought_id', (int)$_POST['bought_id'], PDO::PARAM_INT);
$query->bindValue(':time_invested', (int)$_POST['time_invested'], PDO::PARAM_INT);
$query->execute();
$query = $db_connection->prepare('UPDATE planz_users SET points = points + :points_earned WHERE id = :user_id');
$query->bindValue(':points_earned', (int)$_POST['points_earned'], PDO::PARAM_INT);
$query->bindValue(':user_id', (int)$_SESSION['user_id'], PDO::PARAM_INT);
$query->execute();
$query_check = $db_connection->prepare('SELECT points FROM planz_users WHERE id = :user_id');
$query_check->bindValue(':user_id', (int)$_SESSION['user_id'], PDO::PARAM_INT);
$query_check->execute();
$db_connection->commit();
$_SESSION['points'] = $query_check->fetchColumn();
echo $_SESSION['points'];
Note the use of beginTransaction and commit, as they should speed up the querying.

Related

How to get the value of expression from LAST_INSERT_ID(`my_column`+1)?

DB Type: MariaDB
Table Engine: InnoDB
I have a table where inside it has a column with a value which is being incremented (not auto, no inserting happens in this table)
When I run the following SQL query in phpMyAdmin it works just fine as it should:
UPDATE `my_table`
SET `my_column` = LAST_INSERT_ID(`my_column` + 1)
WHERE `my_column2` = 'abc';
SELECT LAST_INSERT_ID();
The above returns me the last value for the my_column table when the query happened. This query was taken directly from the mysql docs on locking: https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html (to the bottom) and this seems to be the recommended way of working with counters when you don't want it to be affected by other connections.
My PDO:
try {
$conn = new PDO("mysql:host=$servername;dbname=$dbname", $username, $password);
// set the PDO error mode to exception
$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = "UPDATE `my_table`
SET `my_column` = LAST_INSERT_ID(`my_column` + 1)
WHERE `my_column2` = 'abc';
SELECT LAST_INSERT_ID();";
// Prepare statement
$stmt = $conn->prepare($sql);
// execute the query
$stmt->execute();
$result = $stmt->fetchColumn(); // causes general error
$result = $stmt->fetch(PDO::FETCH_ASSOC);// causes general error
// echo a message to say the UPDATE succeeded
echo $stmt->rowCount() . " records UPDATED successfully";
} catch(PDOException $e) {
echo $sql . "<br>" . $e->getMessage();
}
$conn = null;
Exact error SQLSTATE[HY000]: General error, If I remove the lines where I try to get the result, it updates the column, but I still do not have a return result... how do I perform that update query and get the select result all in one go like I do when I run it in phpMyAdmin? This all needs to happen in one go as specified by the MySQL docs so I don't have issues where two connections might get the same counter.
There is no need to perform SELECT LAST_INSERT_ID();. PDO will save that value automatically for you and you can get it out of PDO.
Simply do this:
$conn = new PDO("mysql:host=$servername;dbname=$dbname;charset=utf8mb4", $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
$sql = "UPDATE `my_table`
SET `my_column` = LAST_INSERT_ID(`my_column` + 1)
WHERE `my_column2` = 'abc'";
// Prepare statement
$stmt = $conn->prepare($sql);
// execute the query
$stmt->execute();
$newID = $conn->lastInsertId();
lastInsertId() will give you the value of the argument evaluated by LAST_INSERT_ID().

How to know MySQL update command has found the record or not?

Sometimes I need to know MySQL has found the record or no. I don't mean to know is there affected rows or not, just finding.
$db = database_system::connect();
$query = "UPDATE member SET username=?,password=? WHERE id=?";
$stmt = $db->prepare($query);
$stmt->bind_param("ssi",$this->username,$this->password,$this->id);
$stmt->execute();
$stmt->store_result();
if(!$stmt->affected_rows) throw new Exception("something about");
$stmt->close();
For example, php code above will throw exception if there is no affected record, but I wanna throw exception when there there is no such user (with that ID).
You need to check it in a separate query. The number of affected rows can return zero if the details haven't changed, too. (i.e. updating a valid user ID with the same username & password as it was before)
So best run a separate select for the user, first.
You can set an option to return the number of rows found, instead of the number of rows updated.
For PDO on PHP 5.3 and later:
$p = new PDO($dsn, $u, $p, array(PDO::MYSQL_ATTR_FOUND_ROWS => true));
For MySQLi:
mysqli_real_connect($dsn, $host, $u, $p, $dbname, $port, $socket, MYSQLI_CLIENT_FOUND_ROWS);

Is it possible to run a mysqli query directly via the link while a statement is opened?

I'm trying to do a simple operation on a MySQL database: my contacts have their complete names on a column called first_name while the column last_name is empty.
So I want to take what's on the first_name column and split it on the first occurrence of a white space and put the first part on the first_name column and the second part on the last_name column.
I use the following code but it's not working:
$connection = new mysqli(DATABASE_SERVER, DATABASE_USERNAME, DATABASE_PASSWORD, DATABASE_NAME, DATABASE_PORT);
$statement = $connection->prepare("SELECT id, first_name FROM contacts");
$statement->execute();
$statement->bind_result($row->id, $row->firstName);
while ($statement->fetch()) {
$names = separateNames($row->firstName);
$connection->query('UPDATE contacts SET first_name="'.$names[0].'", last_name="'.$names[1].'" WHERE id='.$row->id);
}
$statement->free_result();
$statement->close();
$connection->close();
Can I use the $connection->query while having the statement open?
Best regards.
UPDATE
The $connection->query(...) returns FALSE and I get the following error:
PHP Fatal error: Uncaught exception 'Exception' with message 'MySQL Error - 2014 : Commands out of sync; you can't run this command now'
I changed the code to the following and worked:
$connection = new mysqli(DATABASE_SERVER, DATABASE_USERNAME, DATABASE_PASSWORD, DATABASE_NAME, DATABASE_PORT);
$result = $connection->query("SELECT id, first_name FROM contacts");
while ($row = $result->fetch_row()) {
$names = separateNames($row[1]);
$connection->query('UPDATE contacts SET first_name="'.$names[0].'", last_name="'.$names[1].'" WHERE id='.$row[0]);
}
$connection->close();
Can I use the $connection->query while having the statement open?
Yes. It will return a new result object or just a boolean depending on the SQL query, see http://php.net/mysqli_query - In your case of running an UPDATE query it will always return a boolean, FALSE if it failed, TRUE if it worked.
BTW, the Mysqli connection object is not the Mysqli statement object, so they normally do not interfere with each other (disconnecting might destroy/break some statements under circumstances, but I would consider this an edge-case for your question you can ignore for the moment).
I wonder why you ask actually. Maybe you should improve the way you do trouble-shooting?
I can only have one active statement at a given time, so I had to make one of the queries via the $connection->query() method.
As #hakre mentioned:
I still keep my suggestion that you should (must!) do prepared statements instead of query() to properly encode the update values
I opted to use the statement method for the update query, so the final working code is the following:
$connection = new mysqli(DATABASE_SERVER, DATABASE_USERNAME, DATABASE_PASSWORD, DATABASE_NAME, DATABASE_PORT);
$result = $connection->query("SELECT id, first_name FROM contacts");
$statement = $connection->prepare("UPDATE contacts SET first_name=?, last_name=? WHERE id=?");
while ($row = $result->fetch_row()) {
$names = separateNames($row[1]);
$statement->bind_param('ssi', $names[0], $names[1], $row[0]);
throwExceptionOnMySQLStatementError($statement, "Could not bind parameters", $logger);
$statement->execute();
throwExceptionOnMySQLStatementError($statement, "Could not execute", $logger);
}
$statement->free_result();
$statement->close();
$connection->close();
Thanks to all that gave their inputs, specially to #hakre that helped me to reach this final solution.

Why does my PHP transaction not work?

I'm working on a school project creating a CMS for my portfolio site. I am having trouble getting my update function to work. I have a feeling it has something to do with the way I'm constructing my PDO Transaction. In my database I have a projects table, category table, and the associative content_category table. I'm able to insert my projects into those tables just fine. What I want to do is insert into my projects table then delete all records from the content_category table and finally insert the current category records into that associative table to complete the transaction. I do get my return statement "Project Updated" returned. But the tables aren't being updated. Any ideas anyone?
Here's the code:
This is a function in my Project class.
public function update(){
try {
$conn = getConnection();
$conn->beginTransaction();
$sql = "UPDATE project
SET project_title = :title,
project_description = :desc,
project_isFeatured = :feat,
project_mainImage = :image
WHERE project_id = :id";
$st = $conn->prepare($sql);
$st->bindValue(":id", $this->id, PDO::PARAM_INT);
$st->bindValue(":title", $this->title, PDO::PARAM_STR);
$st->bindValue(":desc", $this->description, PDO::PARAM_STR);
$st->bindValue(":feat", $this->isFeatured, PDO::PARAM_BOOL);
$st->bindValue(":image", $this->mainImage, PDO::PARAM_INT);
$st->execute();
$sql = "DELETE from content_category
WHERE content_id = :id";
$st = $conn->prepare($sql);
$st->bindValue("id", $this->id, PDO::PARAM_INT);
$st->execute();
$sql = "INSERT into content_category (content_id, cat_id)
VALUES (?,?)";
$st = $conn->prepare($sql);
foreach($this->categories as $key=>$value){
$st->execute(array(intval($projectID), intval($value)));
}
$conn->commit();
$conn = null;
return "Project updated";
}
catch(Exception $e) {
echo $e->getMessage();
$conn->rollBack();
return "Error... Unable to update!";
}
}
Your database engine for the tables needs to be INNODB. If you are using phpMyAdmin, it defaults to MyISAM. (I don't know if that will cause the updates not to go through or just the transaction line to be ignored. Edit: Pretty sure the documentation is saying that it will throw an error and not do anything if you beginTransaction on a myISAM)
In order to make sure you are not encountering a PDO error, you should set the PDO error reporting like this:
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
There are functions in PDO such as prepare() which will either return false or throw a PDOException depending on what error mode is set. This way, it will throw an exception and you'll definitely know if you are having a problem!
Also, if your database doesn't support transactions (like MyISAM), the beginTransaction() function will return false. So, maybe add a check in there like:
if($conn->beginTransaction()) {
// Do transaction here
} else {
echo("Unable to use transactions with this database.");
}
Oddly enough, according to PHP documentation, you would be getting an exception if your database doesn't support transactions...
Unfortunately, not every database supports transactions, so PDO needs to run in what is known as "auto-commit" mode when you first open the connection. Auto-commit mode means that every query that you run has its own implicit transaction, if the database supports it, or no transaction if the database doesn't support transactions. If you need a transaction, you must use the PDO::beginTransaction() method to initiate one. If the underlying driver does not support transactions, a PDOException will be thrown (regardless of your error handling settings: this is always a serious error condition).
Commit returns TRUE on success or FALSE on failure. You can check it. Also check for errorCode.

How can I get the ID of the last INSERTed row using PDO with SQL Server?

Under other circumstances I might be tempted to use
$result = mssql_query("INSERT INTO table (fields) VALUES (data);
SELECT CAST(scope_identity() AS int)");
but as I will be inserting user-submitted data, I want to continue to use PDO, which returns an empty array.
Unfortunately, I'm running PHP on a Linux server and using dblib to interface with Microsoft SQL Server, which doesn't support PDO::lastInsertID().
Please help!
Update to include code example
Here's the code I'm using: col1 is a field of type int identity and col2 is a datetime with a default of getdate().
// Connect to db with PDO
$pdo = new PDO( 'dblib:host=' . $host . ';dbname=' . $database . ';', $username, $password, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION) );
// Connect to db with MSSQL
$sql = mssql_connect( $host, $username, $password );
mssql_select_db( $database, $sql );
// Create SQL statement
$query = "INSERT INTO [table] ( col3, col4, col5 )
VALUES ( 'str1', 'str2', 'str3' );
SELECT SCOPE_IDENTITY() AS theID;";
// Run with MSSQL
echo "Using MSSQL...\n";
$result = mssql_query( $query );
$the_id = mssql_result( $result, 0, 'theID' );
echo "Query OK. Returned ID is " . $the_id . "\n";
// Run with PDO
echo "\nUsing PDO...\n";
$stmt = $pdo->query( $query );
$result = $stmt->fetchAll( PDO::FETCH_ASSOC );
print_r( $result );
And this is what was displayed:
Using MSSQL...
Query OK. Returned ID is 149
Using PDO...
Array
(
)
I would love to find out that I'd done something stupid, rather than come up against a horrible dead end :)
You've got a few choices:
SELECT ##IDENTITY - return the last ID created by actions of the current connection, regardless of table/scope
SELECT SCOPE_IDENTITY() - last ID produced by the current connection, in scope, regardless of table
SELECT IDENT_CURRENT('name_of_table'); - last ID produced on that table, regardless of table/scope/connection
Of the three, SCOPE_IDENTITY() is the best candidate.
Maybe you are getting two rowsets returned. Try adding SET NOCOUNT ON; to eliminate the INSERT's rowset, or use $stmt->nextRowset if your driver supports it.

Categories