8

Here is an example.

$mysqli = new mysqli("localhost", "root", "123", "temp");

$mysqli->begin_transaction();

$sql1 = "insert into test (Name) values ('pratik5');";
$sql1 .= "insert into test (Name) values ('pratik6');";

$test = $mysqli->multi_query($sql1);

$mysqli->commit();

There isn't any error in either of the queries, but when calling commit() the values are not stored in the database. The same works perfectly fine if split into separate queries and executed via query().

Dharman
  • 30,962
  • 25
  • 85
  • 135
Pratik Solanki
  • 392
  • 3
  • 9
  • 1
    Besides the answers given already, I would not use a transaction for this. Keep in mind that transactions blocks may have a bad impact in the performance of other simultaneous connections. Why not just use `insert into test (Name) values ('pratik5'), ('pratik6');`? – Gonzalo Feb 28 '20 at 04:14

3 Answers3

5
$mysqli->multi_query($sql1);
$mysqli->commit(); // This will result in a "Commands out of sync; you can't run this command now" error.

The above is identical to:

$mysqli->multi_query($sql1);
$mysqli->query("commit"); // This will result in a "Commands out of sync; you can't run this command now" error.

Whatever you put in $mysqli->query("...");, it WILL result in a "Commands out of sync" error, even with a simple SELECT 1;

The reason for this error is because ->commit() operation runs a single query (commit;). However, the results of the previous queries have not been read.

When a single query() operation is used, the MySQL server will answer with a response frame that depends on the query statement.

When using multi_query(), the following happens at MySQL communication protocol level:

  1. A "Request Set Option" (as displayed in Wireshark) frame is sent with "multi statements" flag set to ON.
  2. A frame containing the whole string transmitted to multi_query() as request.
  3. MySQL server answers with a response that may contain different resultsets. The same is true when calling a stored procedure.

Solution 1

If you want to use multi_query(), you must have your start transaction / commit operations as part of it:

$mysqli = new mysqli("localhost", "root", "123", "temp");

$sql1 = "start transaction;"; // $mysqli->begin_transaction() is a convenience function for simply doing this.
$sql1 .= "insert into test (Name) values ('pratik5');";
$sql1 .= "insert into test (Name) values ('pratik6');";
$sql1 .= "commit;"; // $mysqli->commit() is a convenience function for simply doing this.

$mysqli->multi_query($sql1);

/* As in "Solution 2", if you plan to perform other queries on DB resource
   $mysqli after this, you must consume all the resultsets:
// This loop ensures that all resultsets are processed and consumed:
do {
    $mysqli->use_result();
}
while ($mysqli->next_result());
*/

Solution 2

$mysqli = new mysqli("localhost", "root", "123", "temp");

$mysqli->begin_transaction();

$sql1 = "insert into test (Name) values ('pratik5');";
$sql1 .= "insert into test (Name) values ('pratik6');";

$mysqli->multi_query($sql1);

// This loop ensures that all resultsets are processed and consumed:
do {
    $mysqli->use_result();
}
while ($mysqli->next_result());

// Now that all resultsets are processed, a single query `commit;` can happen:
$mysqli->commit();

MySQL Reference: "Commands out of sync".

Patrick Allaert
  • 1,751
  • 18
  • 44
  • Are you sure about the error message? I never received such error when I was testing. – Dharman Feb 24 '20 at 13:36
  • 1
    Probably because you are missing testing `$test` which should be `false`, and the error could be accessed with `echo $ysqli->error;` – Patrick Allaert Feb 24 '20 at 14:05
  • You are right. The error is not being thrown automatically. However, even without that error I knew that I had to fetch the results and a better workaround was suggested by YCS. The question asks why can't you use `$mysqli->commit()`. It's interesting that you can use `commit;`, but not the mysqli function. – Dharman Feb 24 '20 at 14:19
  • The [docs](https://www.php.net/manual/en/mysqli.multi-query.php#106126) do not show this... – Luuk Feb 24 '20 at 14:19
  • INSERTs do not produce results, so why do I have to fetch the results before executing `commit()`? – Dharman Feb 24 '20 at 14:22
  • 2
    The communication protocol of MySQL that permits multiple queries does not know about the content. This is also true for `mysqli->multi_query()`. Exactly ONE resultset per individual query will be produced by the server, regardless of the type `SELECT` vs `UPDATE`/`DELETE`/`INSERT`. This means that if they are 2 statements inside `$sql1`, the while loop will be entered twice, put some echo statement in it and you will see it in action. – Patrick Allaert Feb 24 '20 at 14:35
  • @Luuk: There is a comment on that same page that talks about that "Commands out of sync" error: https://www.php.net/manual/en/mysqli.multi-query.php#110155 – Patrick Allaert Feb 24 '20 at 14:37
  • 2
    INSERTs do not produce results, unless when they are put inside a call to `multi_query()`. When `multi_query()` is used, it is not PHP that separates the various queries inside the provided string and sends each of them separately. Its is the underlying MySQL client library that sends a "Request Set Option" with the "multi statements flag" set, so that the next MySQL request contains the whole string that is passed to `multi_query()`. PHP does not analyze the content of what is passed to `multi_query()`, it sends the whole thing **AS IS**. – Patrick Allaert Feb 24 '20 at 14:46
  • 1
    That's all good information. You should incorporate it into your answer. – Dharman Feb 24 '20 at 14:48
  • @Dharman: done. Also slightly corrected the while loop so that it works in every cases. – Patrick Allaert Feb 24 '20 at 15:45
  • the first one shouln't be used – Your Common Sense Feb 24 '20 at 15:55
  • 1
    @YourCommonSense I would say that if possible, `multi_query()` should not be used at all, however, if the goal is to run externally generated script, probably it is the only good-enough solution as custom parsing in PHP would probably result in re-inventing the square wheel. Now, between solution 1 and 2, I would personally recommend the first one. Combining single queries (which is what `begin_transaction()`/`commit()` uses under the hood) and `multi_queries()` sounds very convulsed. Hence why solution 2 uses something that looks a bit hacky to free the resultsets. – Patrick Allaert Feb 24 '20 at 16:13
  • "Hacky" or not but you must use it with the first one as well. – Your Common Sense Feb 24 '20 at 16:14
  • 1
    @YourCommonSense: This is not entirely true, the example I provided works. It "must" only if, using the same connection, you would want to have other subsequent calls to `multi_query()` or `query()`. – Patrick Allaert Feb 24 '20 at 16:22
  • @YourCommonSense Why do you need to use mysqli functions `begin_transaction()/commit()` with the pure SQL solution? – Dharman Feb 24 '20 at 16:26
  • Oh I see you meant that you need to fetch the results in full either way. Yes, that I agree there is no difference between 1st and 2nd solution in this regard. – Dharman Feb 24 '20 at 16:28
  • @Dharman there is no difference how do you call begin and commit. – Your Common Sense Feb 24 '20 at 16:29
  • @YourCommonSense There is a difference in order of operations. You need to do the while after commit in the first solution, but in the 2nd one you need to loop before commit. – Dharman Feb 24 '20 at 16:30
  • 1
    @Dharman I don't know what your intentions are, but we have another misleading shortsighted answer on Stack Overflow that solves one silly non-existent problem and creates a real hell of a problem instead. Fine. – Your Common Sense Feb 24 '20 at 16:32
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/208432/discussion-between-dharman-and-your-common-sense). – Dharman Feb 24 '20 at 16:35
3

You shouldn't use multi query. Rewrite your code as follows

$mysqli->begin_transaction();

$mysqli->query("insert into test (Name) values ('pratik5')");
$mysqli->query("insert into test (Name) values ('pratik6')");

$mysqli->commit();

or, for the real-life inserts,

$mysqli->begin_transaction();

$stmt = $mysqli->prepare("insert into test (Name) values (?)");
$stmt->bind_param("s", $name);
$name = 'pratik5';
$stmt->execute();
$name = 'pratik6';
$stmt->execute();

$mysqli->commit();
Your Common Sense
  • 156,878
  • 40
  • 214
  • 345
  • 1
    May I know the reason? – Danyal Sandeelo Oct 17 '16 at 13:48
  • Is there any documentation where it specifies multi_query having some issues when transactions are used. As I knew the solution u gave. But using your solution would mean my application server sending data to and fro to my database server when i can cumulatively send it for some set of queries. – Pratik Solanki Oct 17 '16 at 13:49
  • it should work as per this http://www.w3schools.com/php/func_mysqli_multi_query.asp – Danyal Sandeelo Oct 17 '16 at 13:50
  • @PratikSolanki I am not finished with the docs yet. Just take it as a rule of thumb. Besides, for the *real* insert you will have to use prepare()/execute() anyway, which ends the argument completely – Your Common Sense Oct 17 '16 at 13:58
  • @YourCommonSense I see, but I would appreciate it Sir if you could tell me the reason? the link that i sent won't work you mean? – Danyal Sandeelo Oct 17 '16 at 18:14
  • I fully agree with this answer, but it still doesn't answer the question why does one need to use `next_result()/store_result()` between the calls to `multi_query()` and `commit()`. – Dharman Feb 21 '20 at 18:53
  • 3
    @YourCommonSense: `multi_query()` is **not** asynchronous that is not the reason of the failure, it is because all result sets should be consumed/freed. – Patrick Allaert Feb 24 '20 at 16:17
0

First thing first, in the example you have shown you should not be using multi_query(). You have two separate statements which should be executed separately. See Your Common Sense's answer

multi_query() should be rarely used. It should be used only in situations when you already have a string composed of multiple queries, which you absolutely trust. Don't ever allow variable input into multi_query()!

Why commit() doesn't work after multi_query()?

Truth be told MySQL does throw an error on commit(), but mysqli is not able to throw the error as exception. Whether it is a bug or a technical limitation I do not know. You can see the error if you check manually $mysqli->error property. You should see an error as follows:

Commands out of sync; you can't run this command now

However, the exception is thrown correctly once you call use_result() or store_result().

$mysqli->multi_query(/* SQLs */);
$mysqli->commit();
$r = $mysqli->use_result(); // Uncaught mysqli_sql_exception: Commands out of sync; you can't run this command now

The reason why MySQL says the commands are out of sync is because you can only execute a single command at a time per each MySQL session. However, multi_query() sends the whole SQL string to MySQL in a single command, and let's MySQL run the queries asynchronously. PHP will initially wait for the first result-set producing non-DML query (not INSERT, UPDATE or DELETE) to finish running before the control is passed back to PHP script. This allows MySQL to run the rest of the SQL on the server and buffer the results, while you can do some other operations in PHP and collect the results later. You can't run another SQL command on the same connection until you iterate over all the results from the previous asynchronous queries.

As pointed out in another answer, commit() will try to execute another command on the same connection. You are getting the out of sync error, because you simply haven't finished processing the multi_query() command.

MySQLi blocking loop

Each asynchronous query should be followed by a blocking loop in mysqli. A blocking loop will iterate over all the executed queries on the server and fetch the results one by one, which you can then process in PHP. Here is an example of such loop:

do {
    $result = $mysqli->use_result();
    if ($result) {
        // process the results here
        $result->free();
    }
} while ($mysqli->next_result()); // Next result will block and wait for next query to finish
$mysqli->store_result(); // Needed to fetch the error as exception

You must have the blocking loop always, even when you know the queries are not going to produce any result-sets.

Solution

Stay away from multi_query()! Most of the time there's a better way of executing SQL files. If you have your queries separate, then don't concatenate them together, and execute each on their own.

If you really need to use multi_query(), and you would like to wrap it in transaction, you must put the commit() after the blocking loop. All the results need to be iterated over before you can execute the COMMIT; command.

$mysqli->begin_transaction();

$sql1 = "insert into test (Name) values ('pratik5');";
$sql1 .= "insert into test (Name) values ('pratik6');";

$test = $mysqli->multi_query($sql1);

// $mysqli->commit();

do {
    $result = $mysqli->use_result();
    if ($result) {
        // process the results here
        $result->free();
    }
} while ($mysqli->next_result());
$mysqli->store_result(); // To fetch the error as exception

$mysqli->commit();

Of course to see any of the mysqli errors you need to enable exception mode. Simply, put this line before new mysqli():

mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
Community
  • 1
  • 1
Dharman
  • 30,962
  • 25
  • 85
  • 135