Commit b713ba74 authored by Marco Pivetta's avatar Marco Pivetta

Merge branch 'fix/#2546-prepared-statement-close-cursor-invalidation-2.5' into 2.5

Backport #2546 to 2.5.x
parents fc376f7a 658e3257
......@@ -48,6 +48,13 @@ class DB2Statement implements \IteratorAggregate, Statement
*/
private $_defaultFetchMode = \PDO::FETCH_BOTH;
/**
* Indicates whether the statement is in the state when fetching results is possible
*
* @var bool
*/
private $result = false;
/**
* DB2_BINARY, DB2_CHAR, DB2_DOUBLE, or DB2_LONG
*
......@@ -104,11 +111,14 @@ class DB2Statement implements \IteratorAggregate, Statement
}
$this->_bindParam = array();
db2_free_result($this->_stmt);
$ret = db2_free_stmt($this->_stmt);
$this->_stmt = false;
return $ret;
if (!db2_free_result($this->_stmt)) {
return false;
}
$this->result = false;
return true;
}
/**
......@@ -151,22 +161,24 @@ class DB2Statement implements \IteratorAggregate, Statement
return false;
}
/*$retval = true;
if ($params !== null) {
$retval = @db2_execute($this->_stmt, $params);
} else {
$retval = @db2_execute($this->_stmt);
}*/
if ($params === null) {
ksort($this->_bindParam);
$params = array_values($this->_bindParam);
$params = array();
foreach ($this->_bindParam as $column => $value) {
$params[] = $value;
}
}
$retval = @db2_execute($this->_stmt, $params);
if ($retval === false) {
throw new DB2Exception(db2_stmt_errormsg());
}
$this->result = true;
return $retval;
}
......@@ -197,6 +209,12 @@ class DB2Statement implements \IteratorAggregate, Statement
*/
public function fetch($fetchMode = null)
{
// do not try fetching from the statement if it's not expected to contain result
// in order to prevent exceptional situation
if (!$this->result) {
return false;
}
$fetchMode = $fetchMode ?: $this->_defaultFetchMode;
switch ($fetchMode) {
case \PDO::FETCH_BOTH:
......
......@@ -80,6 +80,13 @@ class MysqliStatement implements \IteratorAggregate, Statement
*/
protected $_defaultFetchMode = PDO::FETCH_BOTH;
/**
* Indicates whether the statement is in the state when fetching results is possible
*
* @var bool
*/
private $result = false;
/**
* @param \mysqli $conn
* @param string $prepareString
......@@ -168,9 +175,6 @@ class MysqliStatement implements \IteratorAggregate, Statement
if (null === $this->_columnNames) {
$meta = $this->_stmt->result_metadata();
if (false !== $meta) {
// We have a result.
$this->_stmt->store_result();
$columnNames = array();
foreach ($meta->fetch_fields() as $col) {
$columnNames[] = $col->name;
......@@ -178,21 +182,42 @@ class MysqliStatement implements \IteratorAggregate, Statement
$meta->free();
$this->_columnNames = $columnNames;
$this->_rowBindedValues = array_fill(0, count($columnNames), null);
$refs = array();
foreach ($this->_rowBindedValues as $key => &$value) {
$refs[$key] =& $value;
}
if (!call_user_func_array(array($this->_stmt, 'bind_result'), $refs)) {
throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
}
} else {
$this->_columnNames = false;
}
}
if (false !== $this->_columnNames) {
// Store result of every execution which has it. Otherwise it will be impossible
// to execute a new statement in case if the previous one has non-fetched rows
// @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html
$this->_stmt->store_result();
// Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql,
// it will have to allocate as much memory as it may be needed for the given column type
// (e.g. for a LONGBLOB field it's 4 gigabytes)
// @link https://bugs.php.net/bug.php?id=51386#1270673122
//
// Make sure that the values are bound after each execution. Otherwise, if closeCursor() has been
// previously called on the statement, the values are unbound making the statement unusable.
//
// It's also important that row values are bound after _each_ call to store_result(). Otherwise,
// if mysqli is compiled with libmysql, subsequently fetched string values will get truncated
// to the length of the ones fetched during the previous execution.
$this->_rowBindedValues = array_fill(0, count($this->_columnNames), null);
$refs = array();
foreach ($this->_rowBindedValues as $key => &$value) {
$refs[$key] =& $value;
}
if (!call_user_func_array(array($this->_stmt, 'bind_result'), $refs)) {
throw new MysqliException($this->_stmt->error, $this->_stmt->sqlstate, $this->_stmt->errno);
}
}
$this->result = true;
return true;
}
......@@ -240,6 +265,12 @@ class MysqliStatement implements \IteratorAggregate, Statement
*/
public function fetch($fetchMode = null)
{
// do not try fetching from the statement if it's not expected to contain result
// in order to prevent exceptional situation
if (!$this->result) {
return false;
}
$values = $this->_fetch();
if (null === $values) {
return false;
......@@ -325,6 +356,7 @@ class MysqliStatement implements \IteratorAggregate, Statement
public function closeCursor()
{
$this->_stmt->free_result();
$this->result = false;
return true;
}
......
......@@ -80,6 +80,13 @@ class OCI8Statement implements \IteratorAggregate, Statement
*/
private $boundValues = array();
/**
* Indicates whether the statement is in the state when fetching results is possible
*
* @var bool
*/
private $result = false;
/**
* Creates a new OCI8Statement that uses the given connection handle and SQL statement.
*
......@@ -176,7 +183,20 @@ class OCI8Statement implements \IteratorAggregate, Statement
*/
public function closeCursor()
{
return oci_free_statement($this->_sth);
// not having the result means there's nothing to close
if (!$this->result) {
return true;
}
// emulate it by fetching and discarding rows, similarly to what PDO does in this case
// @link http://php.net/manual/en/pdostatement.closecursor.php
// @link https://github.com/php/php-src/blob/php-7.0.11/ext/pdo/pdo_stmt.c#L2075
// deliberately do not consider multiple result sets, since doctrine/dbal doesn't support them
while (oci_fetch($this->_sth));
$this->result = false;
return true;
}
/**
......@@ -229,6 +249,8 @@ class OCI8Statement implements \IteratorAggregate, Statement
throw OCI8Exception::fromErrorInfo($this->errorInfo());
}
$this->result = true;
return $ret;
}
......@@ -257,6 +279,12 @@ class OCI8Statement implements \IteratorAggregate, Statement
*/
public function fetch($fetchMode = null)
{
// do not try fetching from the statement if it's not expected to contain result
// in order to prevent exceptional situation
if (!$this->result) {
return false;
}
$fetchMode = $fetchMode ?: $this->_defaultFetchMode;
if ( ! isset(self::$fetchModeMap[$fetchMode])) {
throw new \InvalidArgumentException("Invalid fetch style: " . $fetchMode);
......@@ -286,6 +314,12 @@ class OCI8Statement implements \IteratorAggregate, Statement
$fetchStructure = OCI_FETCHSTATEMENT_BY_COLUMN;
}
// do not try fetching from the statement if it's not expected to contain result
// in order to prevent exceptional situation
if (!$this->result) {
return array();
}
oci_fetch_all($this->_sth, $result, 0, -1,
self::$fetchModeMap[$fetchMode] | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS);
......@@ -302,6 +336,12 @@ class OCI8Statement implements \IteratorAggregate, Statement
*/
public function fetchColumn($columnIndex = 0)
{
// do not try fetching from the statement if it's not expected to contain result
// in order to prevent exceptional situation
if (!$this->result) {
return false;
}
$row = oci_fetch_array($this->_sth, OCI_NUM | OCI_RETURN_NULLS | OCI_RETURN_LOBS);
if (false === $row) {
......
......@@ -28,6 +28,15 @@ use Doctrine\DBAL\Driver\PDOConnection;
*/
class Connection extends PDOConnection implements \Doctrine\DBAL\Driver\Connection
{
/**
* {@inheritdoc}
*/
public function __construct($dsn, $user = null, $password = null, array $options = null)
{
parent::__construct($dsn, $user, $password, $options);
$this->setAttribute(\PDO::ATTR_STATEMENT_CLASS, array(Statement::class, array()));
}
/**
* @override
*/
......
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\DBAL\Driver\PDOSqlsrv;
use Doctrine\DBAL\Driver\PDOStatement;
use PDO;
/**
* PDO SQL Server Statement
*/
class Statement extends PDOStatement
{
/**
* {@inheritdoc}
*/
public function bindParam($column, &$variable, $type = PDO::PARAM_STR, $length = null, $driverOptions = null)
{
if ($type === PDO::PARAM_LOB && $driverOptions === null) {
$driverOptions = PDO::SQLSRV_ENCODING_BINARY;
}
return parent::bindParam($column, $variable, $type, $length, $driverOptions);
}
/**
* {@inheritdoc}
*/
public function bindValue($param, $value, $type = PDO::PARAM_STR)
{
return $this->bindParam($param, $value, $type);
}
}
......@@ -131,7 +131,7 @@ class SQLAnywhereStatement implements IteratorAggregate, Statement
*/
public function closeCursor()
{
if ( ! sasql_stmt_free_result($this->stmt)) {
if (!sasql_stmt_reset($this->stmt)) {
throw SQLAnywhereException::fromSQLAnywhereError($this->conn, $this->stmt);
}
......
......@@ -53,11 +53,18 @@ class SQLSrvStatement implements IteratorAggregate, Statement
private $stmt;
/**
* Parameters to bind.
* References to the variables bound as statement parameters.
*
* @var array
*/
private $params = array();
private $variables = array();
/**
* Bound parameter types.
*
* @var array
*/
private $types = array();
/**
* Translations.
......@@ -98,6 +105,13 @@ class SQLSrvStatement implements IteratorAggregate, Statement
*/
private $lastInsertId;
/**
* Indicates whether the statement is in the state when fetching results is possible
*
* @var bool
*/
private $result = false;
/**
* Append to any INSERT query to retrieve the last insert id.
*
......@@ -138,11 +152,8 @@ class SQLSrvStatement implements IteratorAggregate, Statement
throw new SQLSrvException("sqlsrv does not support named parameters to queries, use question mark (?) placeholders instead.");
}
if ($type === \PDO::PARAM_LOB) {
$this->params[$column-1] = array($variable, SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY), SQLSRV_SQLTYPE_VARBINARY('max'));
} else {
$this->params[$column-1] = $variable;
}
$this->variables[$column] =& $variable;
$this->types[$column] = $type;
}
/**
......@@ -150,9 +161,20 @@ class SQLSrvStatement implements IteratorAggregate, Statement
*/
public function closeCursor()
{
if ($this->stmt) {
sqlsrv_free_stmt($this->stmt);
// not having the result means there's nothing to close
if (!$this->result) {
return true;
}
// emulate it by fetching and discarding rows, similarly to what PDO does in this case
// @link http://php.net/manual/en/pdostatement.closecursor.php
// @link https://github.com/php/php-src/blob/php-7.0.11/ext/pdo/pdo_stmt.c#L2075
// deliberately do not consider multiple result sets, since doctrine/dbal doesn't support them
while (sqlsrv_fetch($this->stmt));
$this->result = false;
return true;
}
/**
......@@ -193,12 +215,16 @@ class SQLSrvStatement implements IteratorAggregate, Statement
$hasZeroIndex = array_key_exists(0, $params);
foreach ($params as $key => $val) {
$key = ($hasZeroIndex && is_numeric($key)) ? $key + 1 : $key;
$this->bindValue($key, $val);
$this->variables[$key] = $val;
$this->types[$key] = null;
}
}
$this->stmt = sqlsrv_query($this->conn, $this->sql, $this->params);
if ( ! $this->stmt) {
$this->stmt = $this->prepare();
}
if (!sqlsrv_execute($this->stmt)) {
throw SQLSrvException::fromSqlSrvErrors();
}
......@@ -207,6 +233,40 @@ class SQLSrvStatement implements IteratorAggregate, Statement
sqlsrv_fetch($this->stmt);
$this->lastInsertId->setId(sqlsrv_get_field($this->stmt, 0));
}
$this->result = true;
}
/**
* Prepares SQL Server statement resource
*
* @return resource
* @throws SQLSrvException
*/
private function prepare()
{
$params = array();
foreach ($this->variables as $column => &$variable) {
if ($this->types[$column] === \PDO::PARAM_LOB) {
$params[$column - 1] = array(
&$variable,
SQLSRV_PARAM_IN,
SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY),
SQLSRV_SQLTYPE_VARBINARY('max'),
);
} else {
$params[$column - 1] =& $variable;
}
}
$stmt = sqlsrv_prepare($this->conn, $this->sql, $params);
if (!$stmt) {
throw SQLSrvException::fromSqlSrvErrors();
}
return $stmt;
}
/**
......@@ -236,6 +296,12 @@ class SQLSrvStatement implements IteratorAggregate, Statement
*/
public function fetch($fetchMode = null)
{
// do not try fetching from the statement if it's not expected to contain result
// in order to prevent exceptional situation
if (!$this->result) {
return false;
}
$args = func_get_args();
$fetchMode = $fetchMode ?: $this->defaultFetchMode;
......
......@@ -29,6 +29,13 @@ class PDOConnectionTest extends DbalFunctionalTestCase
}
}
protected function tearDown()
{
$this->resetSharedConn();
parent::tearDown();
}
public function testDoesNotRequireQueryForServerVersion()
{
$this->assertFalse($this->driverConnection->requiresQueryForServerVersion());
......
<?php
namespace Doctrine\Tests\DBAL\Functional;
use Doctrine\DBAL\Driver\Statement;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Types\Type;
class StatementTest extends \Doctrine\Tests\DbalFunctionalTestCase
{
protected function setUp()
{
parent::setUp();
$table = new Table('stmt_test');
$table->addColumn('id', 'integer');
$this->_conn->getSchemaManager()->dropAndCreateTable($table);
}
public function testStatementIsReusableAfterClosingCursor()
{
$this->_conn->insert('stmt_test', array('id' => 1));
$this->_conn->insert('stmt_test', array('id' => 2));
$stmt = $this->_conn->prepare('SELECT id FROM stmt_test ORDER BY id');
$stmt->execute();
$id = $stmt->fetchColumn();
$this->assertEquals(1, $id);
$stmt->closeCursor();
$stmt->execute();
$id = $stmt->fetchColumn();
$this->assertEquals(1, $id);
$id = $stmt->fetchColumn();
$this->assertEquals(2, $id);
}
public function testReuseStatementWithLongerResults()
{
$sm = $this->_conn->getSchemaManager();
$table = new Table('stmt_longer_results');
$table->addColumn('param', 'string');
$table->addColumn('val', 'text');
$sm->createTable($table);
$row1 = array(
'param' => 'param1',
'val' => 'X',
);
$this->_conn->insert('stmt_longer_results', $row1);
$stmt = $this->_conn->prepare('SELECT param, val FROM stmt_longer_results ORDER BY param');
$stmt->execute();
$this->assertArraySubset(array(
array('param1', 'X'),
), $stmt->fetchAll(\PDO::FETCH_NUM));
$row2 = array(
'param' => 'param2',
'val' => 'A bit longer value',
);
$this->_conn->insert('stmt_longer_results', $row2);
$stmt->execute();
$this->assertArraySubset(array(
array('param1', 'X'),
array('param2', 'A bit longer value'),
), $stmt->fetchAll(\PDO::FETCH_NUM));
}
public function testFetchLongBlob()
{
// make sure memory limit is large enough to not cause false positives,
// but is still not enough to store a LONGBLOB of the max possible size
$this->iniSet('memory_limit', '4G');
$sm = $this->_conn->getSchemaManager();
$table = new Table('stmt_long_blob');
$table->addColumn('contents', 'blob', array(
'length' => 0xFFFFFFFF,
));
$sm->createTable($table);
$contents = base64_decode(<<<EOF
H4sICJRACVgCA2RvY3RyaW5lLmljbwDtVNtLFHEU/ia1i9fVzVWxvJSrZmoXS6pd0zK7QhdNc03z
lrpppq1pWqJCFERZkUFEDybYBQqJhB6iUOqhh+whgl4qkF6MfGh+s87O7GVmO6OlBfUfdIZvznxn
fpzznW9gAI4unQ50XwirH2AAkEygEuIwU58ODnPBzXGv14sEq4BrwzKKL4sY++SGTz6PodcutN5x
IPvsFCa+K9CXMfS/cOL5OxesN0Wceygho0WAXVLwcUJBdDVDaqOAij4Rrz640XlXQmAxQ16PHU63
iqdvXbg4JOHLpILBUSdM7XZEVDDcfuZEbI2ASaYguUGAroSh97GMngcSeFFFerMdI+/dyGy1o+GW
Ax5FxfAbFwoviajuc+DCIwn+RTwGRmRIThXxdQJyu+z4/NUDYz2DKCsILuERWsoQfoQhqpLhyhMZ
XfcknBmU0NLvQArpTm0SsI5mqKqKuFoGc8cUcjrtqLohom1AgtujQnapmJJU+BbwCLIwhJXyiKlh
MB4TkFgvIK3JjrRmAefJm+77Eiqvi+SvCq/qJahQyWuVuEpcIa7QLh7Kbsourb9b66/pZdAd1voz
fCNfwsp46OnZQPojSX9UFcNy+mYJNDeJPHtJfqeR/nSaPTzmwlXar5dQ1adpd+B//I9/hi0xuCPQ
Nkvb5um37Wtc+auQXZsVxEVYD5hnCilxTaYYjsuxLlsxXUitzd2hs3GWHLM5UOM7Fy8t3xiat4fb
sneNxmNb/POO1pRXc7vnF2nc13Rq0cFWiyXkuHmzxuOtzUYfC7fEmK/3mx4QZd5u4E7XJWz6+dey
Za4tXHUiPyB8Vm781oaT+3fN6Y/eUFDfPkcNWetNxb+tlxEZsPqPdZMOzS4rxwJ8CDC+ABj1+Tu0
d+N0hqezcjblboJ3Bj8ARJilHX4FAAA=
EOF
);
$this->_conn->insert('stmt_long_blob', array(
'contents' => $contents,
), array(\PDO::PARAM_LOB));
$stmt = $this->_conn->prepare('SELECT contents FROM stmt_long_blob');
$stmt->execute();
$stream = Type::getType('blob')
->convertToPHPValue(
$stmt->fetchColumn(),
$this->_conn->getDatabasePlatform()
);
if ($this->_conn->getDriver()->getName() === 'pdo_sqlsrv') {
$this->markTestSkipped('Skipping on pdo_sqlsrv due to https://github.com/Microsoft/msphpsql/issues/270');
}
$this->assertSame($contents, stream_get_contents($stream));
}
public function testIncompletelyFetchedStatementDoesNotBlockConnection()
{
$this->_conn->insert('stmt_test', array('id' => 1));
$this->_conn->insert('stmt_test', array('id' => 2));
$stmt1 = $this->_conn->prepare('SELECT id FROM stmt_test');
$stmt1->execute();
$stmt1->fetch();
$stmt1->execute();
// fetching only one record out of two
$stmt1->fetch();
$stmt2 = $this->_conn->prepare('SELECT id FROM stmt_test WHERE id = ?');
$stmt2->execute(array(1));
$this->assertEquals(1, $stmt2->fetchColumn());
}
public function testReuseStatementAfterClosingCursor()
{
$this->_conn->insert('stmt_test', array('id' => 1));
$this->_conn->insert('stmt_test', array('id' => 2));
$stmt = $this->_conn->prepare('SELECT id FROM stmt_test WHERE id = ?');
$stmt->execute(array(1));
$id = $stmt->fetchColumn();
$this->assertEquals(1, $id);
$stmt->closeCursor();
$stmt->execute(array(2));
$id = $stmt->fetchColumn();
$this->assertEquals(2, $id);
}
public function testReuseStatementWithParameterBoundByReference()
{
$this->_conn->insert('stmt_test', array('id' => 1));
$this->_conn->insert('stmt_test', array('id' => 2));
$stmt = $this->_conn->prepare('SELECT id FROM stmt_test WHERE id = ?');
$stmt->bindParam(1, $id);
$id = 1;
$stmt->execute();
$this->assertEquals(1, $stmt->fetchColumn());
$id = 2;
$stmt->execute();
$this->assertEquals(2, $stmt->fetchColumn());
}
/**
* @dataProvider emptyFetchProvider
*/
public function testFetchFromNonExecutedStatement(callable $fetch, $expected)
{
$stmt = $this->_conn->prepare('SELECT id FROM stmt_test');
$this->assertSame($expected, $fetch($stmt));
}
public function testCloseCursorOnNonExecutedStatement()
{
$stmt = $this->_conn->prepare('SELECT id FROM stmt_test');
$this->assertTrue($stmt->closeCursor());
}
/**
* @dataProvider emptyFetchProvider
*/
public function testFetchFromNonExecutedStatementWithClosedCursor(callable $fetch, $expected)
{
$stmt = $this->_conn->prepare('SELECT id FROM stmt_test');
$stmt->closeCursor();
$this->assertSame($expected, $fetch($stmt));
}
/**
* @dataProvider emptyFetchProvider
*/
public function testFetchFromExecutedStatementWithClosedCursor(callable $fetch, $expected)
{
$this->_conn->insert('stmt_test', array('id' => 1));
$stmt = $this->_conn->prepare('SELECT id FROM stmt_test');
$stmt->execute();
$stmt->closeCursor();
$this->assertSame($expected, $fetch($stmt));
}
public static function emptyFetchProvider()
{
return array(
'fetch' => array(
function (Statement $stmt) {
return $stmt->fetch();
},
false,
),
'fetch-column' => array(
function (Statement $stmt) {
return $stmt->fetchColumn();
},
false,
),
'fetch-all' => array(
function (Statement $stmt) {
return $stmt->fetchAll();
},
array(),
),
);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment