Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix that MysqliStatement cannot handle streams #3217

Merged
merged 1 commit into from
Sep 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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