PDO support for multiple queries (PDO_MYSQL, PDO_MYSQLND)
Asked Answered
N

7

115

I do know that PDO does not support multiple queries getting executed in one statement. I've been Googleing and found few posts talking about PDO_MYSQL and PDO_MYSQLND.

PDO_MySQL is a more dangerous application than any other traditional MySQL applications. Traditional MySQL allows only a single SQL query. In PDO_MySQL there is no such limitation, but you risk to be injected with multiple queries.

From: Protection against SQL Injection using PDO and Zend Framework (June 2010; by Julian)

It seems like PDO_MYSQL and PDO_MYSQLND do provide support for multiple queries, but I am not able to find more information about them. Were these projects discontinued? Is there any way now to run multiple queries using PDO.

Neuralgia answered 14/6, 2011 at 16:15 Comment(3)
Use SQL transactions.Bifurcate
Why would you like to use multiple queries? They aren't transacted, it's just the same as you would execute them one after another. IMHO no pros, only cons. In case of SQLInjection you allow attacker to do whatever he want.Lassie
It's 2020 now, and PDO does support this - see my answer way down below.Seringapatam
D
154

As I know, PDO_MYSQLND replaced PDO_MYSQL in PHP 5.3. Confusing part is that name is still PDO_MYSQL. So now ND is default driver for MySQL+PDO.

Overall, to execute multiple queries at once you need:

  • PHP 5.3+
  • mysqlnd
  • Emulated prepared statements. Make sure PDO::ATTR_EMULATE_PREPARES is set to 1 (default for mysql)

Using exec

$db = new PDO("mysql:host=localhost;dbname=test;charset=utf8mb4", 'root', '');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// works regardless of statements emulation
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, 0);

$sql = "
DELETE FROM car; 
INSERT INTO car(name, type) VALUES ('car1', 'coupe'); 
INSERT INTO car(name, type) VALUES ('car2', 'coupe');
";

$db->exec($sql);

Note that this method has a limited use and only suitable for SQL that contains constant values. When the data is supplied for SQL from PHP variables, prepared statements must be used istead:

Using statements

$db = new PDO("mysql:host=localhost;dbname=test;charset=utf8mb4", 'root', '');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// wouldn't work if set to 0. You can comment out this line as 1 is a default
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, 1);

$sql = "
DELETE FROM car; 
INSERT INTO car(name, type) VALUES (:car1, :type1); 
INSERT INTO car(name, type) VALUES (:car2, :type2);
";

$stmt = $db->prepare($sql);
$stmt->execute(
    ["car1" => "brand1", "type1" => "coupe", "car2" => "brand2", "type2" => "coupe"]
);
// check for errors and collect query results
do {
    echo $pdo->lastInsertId(); // for example
} while ($stmt->nextRowset());

Note that in this case you have loop over query results after executing your statement, in order to check for possible errors or collect the query results, as shown above. In case you don't need to collect any results, the loop can be reduced to just

while ($stmt->nextRowset());

which will still check for errors (provided PDO::ATTR_ERRMODE is set to PDO::ERRMODE_EXCEPTION as shown above).


A note:

When using emulated prepared statements, make sure you have set proper encoding (that reflects actual data encoding) in DSN (available since 5.3.6). Otherwise there can be a slight possibility for SQL injection if some odd encoding is used.

Durkin answered 23/6, 2011 at 22:1 Comment(12)
There is nothing wrong with the answer itself. It explains how to execute multiple queries. Your assumption that the answer is flawed comes from the assumption that query contains user-input. There are valid use cases where sending through multiple queries at once can benefit performance. You could suggest using procedures as an alternative answer to this question, but that does not make this answer bad.Neuralgia
The code in this answer is bad, and promotes some very harmful practices (use of emulation for prepares statements, which make code open to SQL injection vulnerability). Do not use it.Bifurcate
Nothing wrong with this answer, and emulation mode in particular. It is enabled by default in pdo_mysql, and if there were any issue, there already would be thousands of injections. But noone neard of one yet. So it goes.Minicam
In fact, only one who managed to provide not only emotions but some argument, was ircmaxell. However, links he brought are quite irrelevant. First one is inapplicable at all as it explicitly says " PDO is always immune from this bug." While second one is simply solvable by setting proper encoding. So, it deserves a note, not warning, and less appealing one.Minicam
Speaking as someone who is writing a migration tool that uses SQL that only our developers have written (i.e SQL injection is not an issue), this has helped me massively, and any comments indicating that this code is harmful do not fully understand all of the contexts for its usage.Bobettebobina
How do I detect if mysqlnd is being used?Anthroposophy
strpos($pdo->getAttribute(PDO::ATTR_CLIENT_VERSION), 'mysqlnd') !== falseDurkin
Thank you, that was so helpful. I don't know what I would do without stackoverflow! :DXylophagous
This does not work on multiple create trigger queries?Vacla
if this is the case the PHP documentation is wrong.. " PDO::exec -> Execute an SQL statement and return the number of affected rows " an means more or less one right.? But because of ATTR_EMULATE_PREPARES = 1 it might internally run as a loop in the driver code with does separate prepare queries and executions?.. More reason to use PDO::ATTR_EMULATE_PREPARES 0 for safety and let the database PREPARE protocol handle it. .Sickening
this shows the exception but does not help with the rollback of the transactions , if you are crating tables inbetween you script to copy data while partitioning it leaves those tables there although you are using the transaction block and rollback the transaction in the catch blockFolketing
@MuhammadOmerAslam you cannot "rollback" DDL queries like CREATE TABLE. Any such query automatically triggers a commit. dev.mysql.com/doc/refman/8.0/en/implicit-commit.htmlJac
E
22

After half a day of fiddling with this, found out that PDO had a bug where...

--

//This would run as expected:
$pdo->exec("valid-stmt1; valid-stmt2;");

--

//This would error out, as expected:
$pdo->exec("non-sense; valid-stmt1;");

--

//Here is the bug:
$pdo->exec("valid-stmt1; non-sense; valid-stmt3;");

It would execute the "valid-stmt1;", stop on "non-sense;" and never throw an error. Will not run the "valid-stmt3;", return true and lie that everything ran good.

I would expect it to error out on the "non-sense;" but it doesn't.

Here is where I found this info: Invalid PDO query does not return an error

Here is the bug: https://bugs.php.net/bug.php?id=61613


So, I tried doing this with mysqli and haven't really found any solid answer on how it works so I thought I's just leave it here for those who want to use it..

try{
    // db connection
    $mysqli = new mysqli("host", "user" , "password", "database");
    if($mysqli->connect_errno){
        throw new Exception("Connection Failed: [".$mysqli->connect_errno. "] : ".$mysqli->connect_error );
        exit();
    }

    // read file.
    // This file has multiple sql statements.
    $file_sql = file_get_contents("filename.sql");

    if($file_sql == "null" || empty($file_sql) || strlen($file_sql) <= 0){
        throw new Exception("File is empty. I wont run it..");
    }

    //run the sql file contents through the mysqli's multi_query function.
    // here is where it gets complicated...
    // if the first query has errors, here is where you get it.
    $sqlFileResult = $mysqli->multi_query($file_sql);
    // this returns false only if there are errros on first sql statement, it doesn't care about the rest of the sql statements.

    $sqlCount = 1;
    if( $sqlFileResult == false ){
        throw new Exception("File: '".$fullpath."' , Query#[".$sqlCount."], [".$mysqli->errno."]: '".$mysqli->error."' }");
    }

    // so handle the errors on the subsequent statements like this.
    // while I have more results. This will start from the second sql statement. The first statement errors are thrown above on the $mysqli->multi_query("SQL"); line
    while($mysqli->more_results()){
        $sqlCount++;
        // load the next result set into mysqli's active buffer. if this fails the $mysqli->error, $mysqli->errno will have appropriate error info.
        if($mysqli->next_result() == false){
            throw new Exception("File: '".$fullpath."' , Query#[".$sqlCount."], Error No: [".$mysqli->errno."]: '".$mysqli->error."' }");
        }
    }
}
catch(Exception $e){
    echo $e->getMessage(). " <pre>".$e->getTraceAsString()."</pre>";
}
Erythromycin answered 4/3, 2015 at 23:58 Comment(8)
Does it work if you only run $pdo->exec("valid-stmt1; non-sense; valid-stmt3;"); without the previous two execs? I can get it to throw errors in the middle, but not when executed after successful execs.Incommunicative
Nope, it does not. That is the bug with PDO.Erythromycin
My bad, those 3 $pdo->exec("") are independent of each other. I now split them up to indicate that they don't have to be in a sequence for the problem to arise. Those 3 are 3 configurations of running multiple queries in one exec statement.Erythromycin
Interesting. Did you get a chance to see my posted question? I wonder if this has been partially patched because I can get the error thrown if it's the only exec on the page, but if I run multiple exec each with multiple SQL statements in them, then I reproduce the same bug here. But if it's the only exec on the page, then I can't reproduce it.Incommunicative
Did that one exec on your page have multiple statements?Erythromycin
Yes, each exec call contained a string with multiple SQL statements.Incommunicative
At the time I was using PDO for executing multiple statements, I only had one exec(), after instantiating the PDO, with multiple statements in it. And it behaved as described above.Erythromycin
And for my usecase, splitting the queries and running em individually was not an option, so I decided to make the switch to mysqli and discard the PDO code.Erythromycin
S
5

PDO does support this (as of 2020). Just do a query() or prepare() call on a PDO object as usual, separating queries by ; and then nextRowset() to step to the next SELECT result, if you have multiple. Resultsets will be in the same order as the queries. Obviously think about the security implications - so don't accept user supplied queries, use parameters, etc. I use it with queries generated by code for example.

$connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, 1);
$query = "select * from t1 where param=?;select * from t2 where param=?"
$statement = $connection->prepare($query);
$statement->execute([$param1, $param2]);
do {
    $data[] = $statement->fetchAll(PDO::FETCH_ASSOC);
} while ($statement->nextRowset());

Note that in order to use this method, emulated prepared statements must be turned on.

Seringapatam answered 8/7, 2020 at 21:42 Comment(4)
I never would understand this kind of reasoning, "Here is a code that is a big hole in the security neglecting all the recommended good practices so you need to think about the security implications." Who should think about it? When they should think - before using this code or after they get hacked? Why don't you think about it first, before writing this function or offering it to other people?Minicam
Dear @YourCommonSense running multiple queries in one go helps with performance, less network traffic + server can optimise related queries. My (simplified) example only meant to introduce the method required to use it. It's a securtity hole only if you don't use those good practices you are referring to. BTW, I'm suspicious of people who say "I would never understand..." when they easily could... :-)Seringapatam
I noticed that $statement->nextRowset() closes the current transaction, so if something went wrong you can't rollBackKheda
Well, I suppose instead of arguing I should suggest an update that actually applies the security implications. Which was my concern, not running multiple queries per se.Minicam
S
3

A quick-and-dirty approach:

function exec_sql_from_file($path, PDO $pdo) {
    if (! preg_match_all("/('(\\\\.|.)*?'|[^;])+/s", file_get_contents($path), $m))
        return;

    foreach ($m[0] as $sql) {
        if (strlen(trim($sql)))
            $pdo->exec($sql);
    }
}

Splits at reasonable SQL statement end points. There is no error checking, no injection protection. Understand your use before using it. Personally, I use it for seeding raw migration files for integration testing.

Scorbutic answered 6/5, 2015 at 16:26 Comment(1)
This fails if your SQL file contains any mysql builtin commands... It will probably blow out your PHP memory limit too, if the SQL file is large... Splitting on ; breaks if your SQL contains procedure or trigger definitions... Lots of reasons why it's not good.Flaunch
A
2

Like thousands of people, I'm looking for this question:
Can run multiple queries simultaneously, and if there was one error, none would run I went to this page everywhere
But although the friends here gave good answers, these answers were not good for my problem
So I wrote a function that works well and has almost no problem with sql Injection.
It might be helpful for those who are looking for similar questions so I put them here to use

function arrayOfQuerys($arrayQuery)
{
    $mx = true;
    $conn->beginTransaction();
    try {
        foreach ($arrayQuery AS $item) {
            $stmt = $conn->prepare($item["query"]);
            $stmt->execute($item["params"]);
            $result = $stmt->rowCount();
            if($result == 0)
                $mx = false;
         }
         if($mx == true)
             $conn->commit();
         else
             $conn->rollBack();
    } catch (Exception $e) {
        $conn->rollBack();
        echo "Failed: " . $e->getMessage();
    }
    return $mx;
}

for use(example):

 $arrayQuery = Array(
    Array(
        "query" => "UPDATE test SET title = ? WHERE test.id = ?",
        "params" => Array("aa1", 1)
    ),
    Array(
        "query" => "UPDATE test SET title = ? WHERE test.id = ?",
        "params" => Array("bb1", 2)
    )
);
arrayOfQuerys($arrayQuery);

and my connection:

    try {
        $options = array(
            //For updates where newvalue = oldvalue PDOStatement::rowCount()   returns zero. You can use this:
            PDO::MYSQL_ATTR_FOUND_ROWS => true
        );
        $conn = new PDO("mysql:host=$servername;dbname=$database", $username, $password, $options);
        $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    } catch (PDOException $e) {
        echo "Error connecting to SQL Server: " . $e->getMessage();
    }

Note:
This solution helps you to run multiple statement together,
If an incorrect a statement occurs, it does not execute any other statement

Akerboom answered 14/3, 2019 at 18:48 Comment(0)
N
1

Try this function : multiple queries and multiple values insertion.

function employmentStatus($Status) {
$pdo = PDO2::getInstance();

$sql_parts = array(); 
for($i=0; $i<count($Status); $i++){
    $sql_parts[] = "(:userID, :val$i)";
}

$requete = $pdo->dbh->prepare("DELETE FROM employment_status WHERE userid = :userID; INSERT INTO employment_status (userid, status) VALUES ".implode(",", $sql_parts));
$requete->bindParam(":userID", $_SESSION['userID'],PDO::PARAM_INT);
for($i=0; $i<count($Status); $i++){
    $requete->bindParam(":val$i", $Status[$i],PDO::PARAM_STR);
}
if ($requete->execute()) {
    return true;
}
return $requete->errorInfo();
}
Notability answered 17/11, 2016 at 16:0 Comment(0)
S
0

Tried following code

 $db = new PDO("mysql:host={$dbhost};dbname={$dbname};charset=utf8", $dbuser, $dbpass, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));

Then

 try {
 $db->query('SET NAMES gbk');
 $stmt = $db->prepare('SELECT * FROM 2_1_paidused WHERE NumberRenamed = ? LIMIT 1');
 $stmt->execute(array("\xbf\x27 OR 1=1 /*"));
 }
 catch (PDOException $e){
 echo "DataBase Errorz: " .$e->getMessage() .'<br>';
 }
 catch (Exception $e) {
 echo "General Errorz: ".$e->getMessage() .'<br>';
 }

And got

DataBase Errorz: 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 '/*' LIMIT 1' at line 1

If added $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); after $db = ...

Then got blank page

If instead SELECT tried DELETE, then in both cases got error like

 DataBase Errorz: 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 '* FROM 2_1_paidused WHERE NumberRenamed = '¿\' OR 1=1 /*' LIMIT 1' at line 1

So my conclusion that no injection possible...

Seringapatam answered 30/4, 2014 at 17:4 Comment(4)
You should have made it a new question where referencing to this oneMinicam
Not so much question as a result of what i tried. And my conclusion. Initial question is old, possibly not actual at the moment.Seringapatam
Not sure how this is relevant to anything in the question.Upspring
in question are words but you risk to be injected with multiple queries. My answer is about injectionSeringapatam

© 2022 - 2024 — McMap. All rights reserved.