-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Prepared statements become unusable after calling closeCursor() on IBM DB2, Oracle and MS SQL Server. #2546
Prepared statements become unusable after calling closeCursor() on IBM DB2, Oracle and MS SQL Server. #2546
Conversation
@morozov the patch looks legit. What is the matter with mysqli? Why is it not working? Can we get it behaving correctly somehow? |
5505093
to
923c29d
Compare
@deeky666 please see the update. |
$value = $stmt->fetchColumn(); | ||
} catch (\Exception $e) { | ||
// some adapters trigger PHP error or throw adapter-specific exception in case of fetching | ||
// from a closed cursor, which still proves that it has been closed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an inconsistency then. Maybe we should fix that too. The interface states that false
is returned in that case. Having the drivers behave consistently finally would be great. Can you investigate? Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current state is:
- pdo_sqlite returns
false
, - mysqli throws
Doctrine\DBAL\Driver\Mysqli\MysqliException
"Commands out of sync; you can't run this command now", - oci8 returns
false
, - sqlsrv returns
false
, - ibm_db2 triggers warning "db2_fetch_array(): Fetch Failure".
Looks like MySQL and DB2 consider this situation logically exceptional, as the cursor is not just empty but is explicitly closed. Maybe so should we in the rest of the drivers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@morozov it is indeed an exceptional case but we should stick to our interfaces as those are the contracts we provide. In the 3.0
we will rework the interfaces to throw meaningful exceptions as returning false
is not a good behaviour in exceptional cases. But for now we should stick to returning false
I think. @Ocramius thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@deeky666 if I understand correctly, there's no clear expectation of how cursors should behave after being closed given the current implementation. The problem with returning false
is that it's hard to identify why exactly fetch failed on the application end. "Fetch Failure" and "Commands out of sync;" are too generic and may mean some other exception which we don't want to silently convert to false
.
Another question is, do we really have to fix consistency as part of this ticket. It's already big enough and judging by the number resolves ~5 different issues (the number of test cases).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@morozov you are completely right. I feel bad about returning false
too but unfortunately we have made up that contract when designing the API.
I'm also aware that this PR is growing big already but in the end it is about fixing the close cursor behaviour across all the drivers. Unfortunately it's quite a rabbit hole but it feels wrong to leave some parts buggy when trying to fix a general problem (which this test case exposes). I am ok with a rather big change here if we get it fixed consistently.
Also relying on exceptions from some drivers but not from others in this scenario is not an issue for us because it is undefined behaviour (as not being defined in the interface).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@deeky666 I see your point. How should we implement handling these warnings/exceptions? Just do string/error code comparison? Or maybe manually track calls to closeCursor()
in the drivers in question and return false
without calling the actual fetch()
to meet the contract.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@morozov We could use a flag $result
for those problematic drivers, which is set in execute
as soon as we have a result and have fetch()
methods check for that flag and return false
early. closeCursor
will then reset the flag. Does that make sense or am I missing something here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@deeky666 I was thinking about the opposite: having an $isCursorClosed
flag which is set in closeCursor()
, reset in execute()
and is checked before calling the underlying fetch()
implementation. Here, we only want to manually handle the case when the cursor is closed and then fetched. Not the cases when for example the statement is constructed but not yet executed (which would also be covered with your approach). In general, both are about the same. What would you suggest?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@morozov I don't have a strong opinion about either of both options. I am just concerned about what the actual behaviour should be when fetching without having executed the query before. Because IMO the whole API defined in Doctrine\DBAL\Driver\ResultStatement
is in theory only available after executing the prepared statement. I think of it as if one would return a Result
object (which is in fact a cursor) form the execute()
method. So to say an encapsulation of the result cursor. I think APIs like JDBC have similar constucts. So in this case the $result
flag would make more sense to me as it is explicitly set when having a result rather than having as $isCursorClosed
which does not have a meaning as long as we the prepared statment is not executed. I think I saw something like the $result
flag in some driver already...
$stmt->execute(); | ||
|
||
$id = $stmt->fetchColumn(); | ||
$this->assertNotEmpty($id); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to assert here again as it already is covered by the test before. So simply executing and immediately closing should be enough.
$table->addColumn('id', 'integer'); | ||
$sm->createTable($table); | ||
$this->_conn->insert('stmt_test_no_results', array('id' => 1)); | ||
$this->_conn->insert('stmt_test_no_results', array('id' => 2)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We only need one row, so please remove this one.
923c29d
to
c9b86c2
Compare
@deeky666 please see the update. |
@deeky666, ping |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We also need to change sasql_stmt_free_result
to sasql_stmt_reset
in SQLAnywhereStatement::closeCursor()
as reusing the prepared statement won't work otherwise.
Furthermore I would suggest that we introduce more tests for edge case scenarios:
fetch*()
closeCursor()
closeCursor()
->fetch*()
execute()
->closeCursor()
->fetch*()
What do you think? Anything else that comes to mind?
$sm = $this->_conn->getSchemaManager(); | ||
$table = new Table('stmt_test_longer_results'); | ||
$table->addColumn('param', 'string'); | ||
$table->addColumn('value', 'text'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using value
errors on DB2 as it is a reserved word. Using val
for example works though.
|
||
$sm = $this->_conn->getSchemaManager(); | ||
$table = new Table('stmt_test_long_blob'); | ||
$table->addColumn('data', 'blob', array( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using data
errors on DB2 as it is a reserved word, we need to use something else here.
// @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)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to work with $result
here too, otherwise calling closeCursor()
or one of the fetch*()
methods before execute()
errors:
ORA-24374: define not done before fetch or execute and fetch
PDO returns true
for closeCursor()
and false
for fetch*()
methods. We should try to be consistent here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@@ -150,9 +150,13 @@ public function bindParam($column, &$variable, $type = null, $length = null) | |||
*/ | |||
public function closeCursor() | |||
{ | |||
if ($this->stmt) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If $stmt
is not initialized, we need to return true
here. This happens if execute()
has not been called before. Otherwise sqlsrv_fetch()
will error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@@ -150,9 +150,13 @@ public function bindParam($column, &$variable, $type = null, $length = null) | |||
*/ | |||
public function closeCursor() | |||
{ | |||
if ($this->stmt) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also reusing prepared statements does currently not work as the binding by reference does not work. We need to modify bindParam()
to this:
public function bindParam($column, &$variable, $type = null, $length = null)
{
if (!is_numeric($column)) {
throw new SQLSrvException("sqlsrv does not support named parameters to queries, use question mark (?) placeholders instead.");
}
$this->values[$column] = $variable;
if ($type === \PDO::PARAM_LOB) {
$this->params[$column-1] = array(&$this->values[$column], SQLSRV_PARAM_IN, SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY), SQLSRV_SQLTYPE_VARBINARY('max'));
} else {
$this->params[$column-1] =& $this->values[$column];
}
}
Also introduces a new private property $values
that keeps track of parameter references. Otherwise using different parameters won't work and the driver will always use those from the first binding.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@deeky666 is it about the case like:
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());
}
Then it should be $this->values[$column] =& $variable
, same as in MysqliStatement::bindParam()
. In any case, DB2 is unhappy about this case because the DBAL does both db2_bind_param()
and passes the values of $this->_bindParam
to db2_execute()
:
db2_execute(): Binding Error 2
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@morozov basically it is about that yes. But the test case I used was this:
1) Doctrine\Tests\DBAL\Functional\StatementTest::testReuseStatementAfterClosingCursor
Failed asserting that 1 matches expected 2.
/php/srv/tests/Doctrine/Tests/DBAL/Functional/StatementTest.php:151
But also we are talking about SQL Server here, not DB2.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@deeky666 with your fix testReuseStatementAfterClosingCursor()
passes, thank you. Not sure if this is relevant to this issue (I wish it wasn't), but testReuseStatementWithParameterBoundByReference()
above still doesn't pass on SQL Server and IBM DB2. It's made up and I don't know if it should pass in the first place (however it does on other DBs).
The difference between the two is that the first binds param by value, and the second binds them by reference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UPD: I've modified your solution to make testReuseStatementWithParameterBoundByReference()
also pass.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@morozov I now figured out the issue with DB2 binding and it is quite nasty -.-. The binding error seems to be related due to $this->_bindParam
having referenced variable values so the fix that worked for me is to change the portion in execute()
to:
if ($params === null) {
ksort($this->_bindParam);
$params = array();
foreach ($this->_bindParam as $column => $value) {
$params[] = $value;
}
}
This fixes the binding error but in fact we cannot get db_bind_param()
to work at all because it is not actually binding immediately at call time. Instead when calling db2_execute()
, the function looks up variables by the names passed to db2_bind_param
lazily. And even worse, it expects thos variables to be declared in global scope (WTF). In theory we could create global variables on binding with global
keyword but that does not seem to work properly either as I am getting db2_execute(): Binding Error 1
then. So I would suggest changing the code as shown above, because real parameter binding does not work atm anyways.
…db2 and oci drivers
…event string truncation
89bc749
to
ec8c742
Compare
@deeky666 thank you for the detailed review. |
ec8c742
to
bc62b08
Compare
@morozov we are getting closer :) Awesome test cases btw! |
…LongBlob()` on pdo_sqlsrv The test is partially skipped due to: 1. Fetching blob as binary string from pdo_sqlsrv requires using `PDOStatement::bindColumn()` and `PDOStatement::fetch(PDO::FETCH_BIND)` which are not supported by the DBAL. 2. Setting encoding on the connection level is not supported: https://msdn.microsoft.com/en-us/library/ff628164(v=sql.105).aspx
…StatementWithParameterBoundByReference()` on ibm_db2
2050ed7
to
dab3ae1
Compare
Random comment, but it has to be said: @morozov thanks for the patience, impressive work! |
@Ocramius my pleasure. I want to say the same to you guys but I'll wait until this one gets merged :-) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
simply: AWESOME!
LGTM! I'll let have @Ocramius have a second look on that and then we can merge. 🎉 |
@deeky666 does it make sense to introduce a libmysql label as it's also an environment attribute (often not taken into account) which the DBAL behavior may depend on? |
@morozov good point. Done. Added |
…2.5' into 2.5 Backport #2546 to 2.5.x
Backported to |
The adapters above free their statement resources as part of
closeCursor()
, so the statement cannot be reused after that. The rest of adapters (mysqli
,sasql
andPDO
) keep their statement resources in place during closing cursor and allow reuse after.The fact that the statement is closed during
closeCursor()
contradicts the purpose of the method: