0

I have this query:

START TRANSACTION;
UPDATE users SET events = 0 WHERE id = 10;
UPDATE events SET seen = 1 WHERE author_id = 10 AND seen is NULL;
COMMIT;

When I execute that at PHPMyadmin, it works as well. but when I want to execute that by PHP:

$stm = $dbh->prepare("START TRANSACTION;
                        UPDATE users SET events = 0 WHERE id = ?;
                        UPDATE events SET seen = 1 WHERE author_id = ? AND seen is NULL;
                        COMMIT;");
$stm->execute(array($user_id, $user_id));

it throws an error:

Fatal error: Uncaught exception 'PDOException' with message 'SQLSTATE[42000]: Syntax error or access violation: 1064 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 'UPDATE users SET events = 0 WHERE id = ?; UPDATE events SET seen = 1 ' at line 2' in C:\xampp\htdocs\myweb\really_test.php:306 Stack trace: #0 C:\xampp\htdocs\myweb\really_test.php(306): PDO->prepare('START TRANSACTI...') #1 {main} thrown in C:\xampp\htdocs\myweb\really_test.php on line 306

How can I fix it?

Martin AJ
  • 6,261
  • 8
  • 53
  • 111

2 Answers2

5

PDO doesn't support multiple queries when PDO::ATTR_EMULATE_PREPARES are turned off. In PHP 5.5.21+ there's a driver specific constant for turning multiqueries on/off in PDO::query and PDO::prepare called PDO::MYSQL_ATTR_MULTI_STATEMENTS.

However...

Because you want transactions it's safer to rely on the interface methods (PDO::beingTrasaction(), PDO::commit(), and PDO::rollBack()) rather than doing so through your SQL code. First let me demonstrate by using multiple statement objects in case you have emulated prepares turned off.

Using multiple PDO statement objects

// prepare the statements for the transaction
$stm1 = $dbh->prepare("UPDATE users SET events = 0 WHERE id = ?");
$stm2 = $dbh->prepare("UPDATE events SET seen = 1 WHERE author_id = ? AND seen is NULL");
// Make sure PDO is in exception mode
$dbh->setAttribute (PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
try {
    // begin the transaction here you can rollback from the catch block
    $dbh->beginTransaction();
    $stmt1->execute([$user_id]);
    $stmt2->execute([$user_id]);
    $dbh->commit(); // all went OK commit
} catch(PDOException $e) {
    // something went wrong: roll back and handle errors here
    $dbh->rollBack();
}

See the PHP manual on PDO Transactions and auto-commit for more details.

Using a single PDO statement object

To do it with a single prepared statement using multiple queries we need to use emulated prepares...

// These driver options need to be set in the constructor
$opts = [
    PDO::ATTR_EMULATE_PREPARES => true,
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::MYSQL_ATTR_MULTI_STATEMENTS => true,
];
$dbh = new PDO("mysql:host=localhost;dbname=$dbname", $username, $pass, $opts);

// prepare the statements for the transaction
$stmt = $dbh->prepare(
    "UPDATE users SET events = 0 WHERE id = ?;" .
    "UPDATE events SET seen = 1 WHERE author_id = ? AND seen is NULL;"
);


try {
    // begin the transaction here you can rollback from the catch block
    $dbh->beginTransaction();
    $stmt->execute([$user_id, $user_id]);
    $stmt->nextRowSet(); // move to the next rowset
    $dbh->commit(); // all went OK commit
} catch(PDOException $e) {
    // something went wrong: roll back and handle errors here
    $dbh->rollBack();
}
Community
  • 1
  • 1
Sherif
  • 11,786
  • 3
  • 32
  • 57
  • thank you .. upvote. just why did you pass `$id` like that? why not use prepare statement like `?` or `:id` ? – Martin AJ Aug 30 '16 at 01:32
  • I just copied the example from the manual. I almost never use transactions and not even sure if they work the same way with prepared statements. You might want to read the manual carefully about that. I know it has all kinds of gotchyas depending on the driver. – Sherif Aug 30 '16 at 01:34
  • On second thought, I just tried it and seems it works fine (at least in mysql according to my very primitive test). – Sherif Aug 30 '16 at 01:38
  • 2
    Here, I updated my answer to use prepared statements instead. – Sherif Aug 30 '16 at 01:42
1

There are two ways to solve your problem.

One is like in the other answer, but with less fuss about it. If emulation is turned off, then you can't run multiple queries at once and therefore have to run them one by one. That's just a general rule for any multiple query set.

Although it is recommended to use PDO's built-in commands for transactions, you still can do it using raw SQL. Just split your bulk statement into separate queries:

try {
    $dbh->query("START TRANSACTION");
    $dbh->prepare("UPDATE users SET events = 0 WHERE id = ?")->execute([$user_id]);
    $dbh->prepare("UPDATE events SET seen = 1 WHERE author_id = ? AND seen is NULL")->execute([$user_id]);
    $dbh->query("COMMIT");
} catch(PDOException $e) {
    // something went wrong: roll back and handle errors here
    $dbh->rollBack();
    // ALWAYS re-throw an exception or you will never know what went wrong
    throw $e;
}

Another way is just turning emulation on:

$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, TRUE);

This way your statement of multiple queries will be executed all right.

Your Common Sense
  • 156,878
  • 40
  • 214
  • 345
  • 1
    What about the third way? ;-) – Strawberry Aug 30 '16 at 06:50
  • Multi-statements are a feature that should, of course, never be used under any circumstances. Seriously, never, ever use this option. [`Robert'; DROP TABLE students; --`](http://xkcd.com/327/) should be sufficient to illustrate. – Michael - sqlbot Aug 31 '16 at 00:57
  • @Michael-sqlbot unfortunately, you don't understand the meaning of SQL injection. Such a silly secondary query is not the only and even not the most disastrous SQL injection exploit. Forbidding multiple queries won't save you from disaster. You should protect from injection, not exploit. – Your Common Sense Aug 31 '16 at 02:24
  • Aside from the assertion that I don't understand, I completely agree with what you are saying... but MySQL does not support prepared multi-statements. That's why emulation has to be turned on, and "emulation" is nothing more than statement escaping by the client library, a far cry from genuine prepared statements... which is why I was so surprised you mentioned it without a disclaimer, or at all. It only takes one careless developer injecting the word `DESC` in a search query from a form post to leave the barn door wide open. This feature has absolutely no justifiable use case, ever. – Michael - sqlbot Aug 31 '16 at 03:45
  • @Michael-sqlbot sorry, I don't understand what are you talking about. What does this DESC thing to do with multiple statements or emulation? – Your Common Sense Aug 31 '16 at 03:50
  • You can't use a placeholder for `ASC`/`DESC` in an `ORDER BY`, as I am sure you are aware. A developer runs into that, decides to inject it into the string, and to do that, he uses a variable containing "ASC" or "DESC." Subsequent code changes result in setting this variable not with a test of a form parameter, but directly from a form parameter. It's not a quoted string, so no escaping needed. It "works fine." With multi-statements enabled, the vulnerability thus introduced by multiple developers across time is much larger. And genuine prepared multi-statements do not exist in MySQL. – Michael - sqlbot Aug 31 '16 at 04:00
  • @Michael-sqlbot Whatever issue with DESC keyword has nothing to do with prepared statements. It seems that your fears are boil down back to the exploit of injection. But again, this particular exploit you are aware of is not the only exploit possible. There are others. Millions of them. You can't cover your back by forbidding multiple queries. You should protect from SQL injections in general, not from just one particular exploit. You are confusing the vulnerability with the exploit – Your Common Sense Aug 31 '16 at 04:36
  • I guess we are not communicating, because, once again, I agree with your points. My simple claim is that enabing multi-statements increases the scope of a system's vulnerability if an exploit occurs, through any mechanism. – Michael - sqlbot Aug 31 '16 at 12:17
  • @Michael-sqlbot well, my point is that the scope is vast enough even without multi-statements. Compared to amount of otherwise possible exploits it's just a drop in the ocean. I really don't see a point in forbidding multi queries if you already have an injection. You'll get a tornado, the fire and a quake but escape the flood. Will it make you happier? – Your Common Sense Aug 31 '16 at 14:13