Skip to content

Commit

Permalink
Merge pull request #3217 from mpdude/fix-mysqli-blobs
Browse files Browse the repository at this point in the history
Fix that MysqliStatement cannot handle streams
  • Loading branch information
Ocramius authored Sep 27, 2018
2 parents 6891e73 + 72dcd04 commit 9355a2b
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 7 deletions.
68 changes: 66 additions & 2 deletions lib/Doctrine/DBAL/Driver/Mysqli/MysqliStatement.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@

use Doctrine\DBAL\Driver\Statement;
use Doctrine\DBAL\Driver\StatementIterator;
use Doctrine\DBAL\Exception\InvalidArgumentException;
use Doctrine\DBAL\FetchMode;
use Doctrine\DBAL\ParameterType;
use function array_combine;
use function array_fill;
use function count;
use function feof;
use function fread;
use function get_resource_type;
use function is_resource;
use function str_repeat;

/**
Expand All @@ -42,7 +47,7 @@ class MysqliStatement implements \IteratorAggregate, Statement
ParameterType::BOOLEAN => 'i',
ParameterType::NULL => 's',
ParameterType::INTEGER => 'i',
ParameterType::LARGE_OBJECT => 's',
ParameterType::LARGE_OBJECT => 'b',
];

/**
Expand Down Expand Up @@ -169,9 +174,11 @@ public function execute($params = null)
throw new MysqliException($this->_stmt->error, $this->_stmt->errno);
}
} else {
if (! $this->_stmt->bind_param($this->types, ...$this->_bindedValues)) {
list($types, $values, $streams) = $this->separateBoundValues();
if (! $this->_stmt->bind_param($types, ...$values)) {
throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
}
$this->sendLongData($streams);
}
}

Expand Down Expand Up @@ -228,6 +235,63 @@ public function execute($params = null)
return true;
}

/**
* Split $this->_bindedValues into those values that need to be sent using mysqli::send_long_data()
* and those that can be bound the usual way.
*
* @return array<int, array<int|string, mixed>|string>
*/
private function separateBoundValues()
{
$streams = $values = [];
$types = $this->types;

foreach ($this->_bindedValues as $parameter => $value) {
if (! isset($types[$parameter - 1])) {
$types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
}

if ($types[$parameter - 1] === static::$_paramTypeMap[ParameterType::LARGE_OBJECT]) {
if (is_resource($value)) {
if (get_resource_type($value) !== 'stream') {
throw new InvalidArgumentException('Resources passed with the LARGE_OBJECT parameter type must be stream resources.');
}
$streams[$parameter] = $value;
$values[$parameter] = null;
continue;
} else {
$types[$parameter - 1] = static::$_paramTypeMap[ParameterType::STRING];
}
}

$values[$parameter] = $value;
}

return [$types, $values, $streams];
}

/**
* Handle $this->_longData after regular query parameters have been bound
*
* @throws MysqliException
*/
private function sendLongData($streams)
{
foreach ($streams as $paramNr => $stream) {
while (! feof($stream)) {
$chunk = fread($stream, 8192);

if ($chunk === false) {
throw new MysqliException("Failed reading the stream resource for parameter offset ${paramNr}.");
}

if (! $this->_stmt->send_long_data($paramNr - 1, $chunk)) {
throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
}
}
}
}

/**
* Binds a array of values to bound parameters.
*
Expand Down
83 changes: 78 additions & 5 deletions tests/Doctrine/Tests/DBAL/Functional/BlobTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
namespace Doctrine\Tests\DBAL\Functional;

use Doctrine\DBAL\Driver\PDOSqlsrv\Driver as PDOSQLSrvDriver;
use Doctrine\DBAL\FetchMode;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Types\Type;
use const CASE_LOWER;
use function array_change_key_case;
use function fopen;
use function in_array;
use function str_repeat;
use function stream_get_contents;

/**
Expand Down Expand Up @@ -49,6 +51,28 @@ public function testInsert()
self::assertEquals(1, $ret);
}

public function testInsertProcessesStream()
{
if (in_array($this->_conn->getDatabasePlatform()->getName(), ['oracle', 'db2'], true)) {
// https://github.com/doctrine/dbal/issues/3288 for DB2
// https://github.com/doctrine/dbal/issues/3290 for Oracle
$this->markTestIncomplete('Platform does not support stream resources as parameters');
}

$longBlob = str_repeat('x', 4 * 8192); // send 4 chunks
$this->_conn->insert('blob_table', [
'id' => 1,
'clobfield' => 'ignored',
'blobfield' => fopen('data://text/plain,' . $longBlob, 'r'),
], [
ParameterType::INTEGER,
ParameterType::STRING,
ParameterType::LARGE_OBJECT,
]);

$this->assertBlobContains($longBlob);
}

public function testSelect()
{
$this->_conn->insert('blob_table', [
Expand Down Expand Up @@ -86,14 +110,63 @@ public function testUpdate()
$this->assertBlobContains('test2');
}

public function testUpdateProcessesStream()
{
if (in_array($this->_conn->getDatabasePlatform()->getName(), ['oracle', 'db2'], true)) {
// https://github.com/doctrine/dbal/issues/3288 for DB2
// https://github.com/doctrine/dbal/issues/3290 for Oracle
$this->markTestIncomplete('Platform does not support stream resources as parameters');
}

$this->_conn->insert('blob_table', [
'id' => 1,
'clobfield' => 'ignored',
'blobfield' => 'test',
], [
ParameterType::INTEGER,
ParameterType::STRING,
ParameterType::LARGE_OBJECT,
]);

$this->_conn->update('blob_table', [
'id' => 1,
'blobfield' => fopen('data://text/plain,test2', 'r'),
], ['id' => 1], [
ParameterType::INTEGER,
ParameterType::LARGE_OBJECT,
]);

$this->assertBlobContains('test2');
}

public function testBindParamProcessesStream()
{
if (in_array($this->_conn->getDatabasePlatform()->getName(), ['oracle', 'db2'], true)) {
// https://github.com/doctrine/dbal/issues/3288 for DB2
// https://github.com/doctrine/dbal/issues/3290 for Oracle
$this->markTestIncomplete('Platform does not support stream resources as parameters');
}

$stmt = $this->_conn->prepare("INSERT INTO blob_table(id, clobfield, blobfield) VALUES (1, 'ignored', ?)");

$stream = null;
$stmt->bindParam(1, $stream, ParameterType::LARGE_OBJECT);

// Bind param does late binding (bind by reference), so create the stream only now:
$stream = fopen('data://text/plain,test', 'r');

$stmt->execute();

$this->assertBlobContains('test');
}

private function assertBlobContains($text)
{
$rows = $this->_conn->fetchAll('SELECT * FROM blob_table');
$rows = $this->_conn->query('SELECT blobfield FROM blob_table')->fetchAll(FetchMode::COLUMN);

self::assertCount(1, $rows);
$row = array_change_key_case($rows[0], CASE_LOWER);

$blobValue = Type::getType('blob')->convertToPHPValue($row['blobfield'], $this->_conn->getDatabasePlatform());
$blobValue = Type::getType('blob')->convertToPHPValue($rows[0], $this->_conn->getDatabasePlatform());

self::assertInternalType('resource', $blobValue);
self::assertEquals($text, stream_get_contents($blobValue));
Expand Down

0 comments on commit 9355a2b

Please sign in to comment.