-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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 last insert ID consistency across drivers #3074
Conversation
71b1239
to
547f921
Compare
547f921
to
f9cf17a
Compare
Looks like we have a connection leak in general and especially with PDO drivers. Here's the number of MySQL connections obtained from
|
d335808
to
6fb67c9
Compare
Almost there. Down to one failure on SQL Server. |
…rever holes have been forgotten
5746e9f
to
8d1ebf7
Compare
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 am not sure about the possible BC impact especially around those type changes (making everything string). :|
return $this->value; | ||
} | ||
|
||
public function register(string $value) : void |
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'm not a fan of having this crate mutable.
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 can make it immutable. Would it make more sense to rename this to something like with() : self
in this case? What about get()
? Should it be something like toValue()
instead?
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.
@Majkl578, sadly, the immutable approach will not just work since MysqliConnection
and MysqliStatement
share the same instance of LastInsertId
. It's probably done like this to mitigate:
MySQL drivers reset the last insert ID to
0
after each non-insert query
It could be worked around by keeping the reference to the connection from the statement. But this approach stinks too (also implemented in PDO Statement/Connection pair).
@@ -32,6 +31,9 @@ | |||
*/ | |||
class PDOConnection extends PDO implements Connection, ServerInfoAwareConnection | |||
{ | |||
/** @var string */ | |||
private $lastInsertId = '0'; |
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 default value + doc block don't seem valid since you assign LastInsertId below.
$this->sql .= self::LAST_INSERT_ID_SQL; | ||
$this->lastInsertId = $lastInsertId; | ||
} | ||
$this->lastInsertId = $lastInsertId; |
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.
Shouldn't this use Driver\LastInsertId too instead of Driver\SQLSrv\LastInsertId? For BC, Driver\SQLSrv\LastInsertId can exend Driver\LastInsertId and be deprecated.
[same for SQLSrvConnection]
|
||
private function trackLastInsertId() : void | ||
{ | ||
if (! $this->lastInsertId) { |
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.
=== null
@@ -155,7 +170,7 @@ public function lastInsertId($name = null) | |||
$stmt = $this->query('SELECT @@IDENTITY'); | |||
} | |||
|
|||
return $stmt->fetchColumn(); | |||
return $stmt->fetchColumn() ?: '0'; |
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 like?:
here, it hides possible error values (would that be false
, '0'
or 0
here?) - is it possible to use !== false
for example?
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.
In the original proposal, lastInsertId()
suppresses errors everywhere by design (see the OCI implementation specifically). Not sure I understand and agree with it. We definitely could improve here.
{ | ||
if (null === $name) { | ||
return sasql_insert_id($this->connection); | ||
return (string) sasql_insert_id($this->connection); |
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.
Looks BC break-ish, what returned false
before would now return ''
.
} | ||
|
||
return $this->query('SELECT ' . $name . '.CURRVAL')->fetchColumn(); | ||
return (string) $this->query('SELECT ' . $name . '.CURRVAL')->fetchColumn(); |
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.
Looks BC break-ish, what returned false
before would now return ''
.
* | ||
* @throws \PDOException | ||
*/ | ||
public function trackLastInsertId() : void |
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 whole design decision behind this method is a bit shady - making subclasses override/disable this behavior looks a bit weird. Would love to see this extracted into some InsertIdStrategyTracking with multiple providers (no-op/identity/...).
{ | ||
if ($name === null) { | ||
return false; | ||
return '0'; |
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.
Looks BC break-ish, what returned false
before would now return '0'
.
} | ||
|
||
return (int) $result; | ||
return (string) $stmt->fetchColumn(); |
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.
Looks BC break-ish, what returned 0
before would now return '0'
.
…rever holes have been forgotten
…rever holes have been forgotten
Given the amount of remaining work which is entirely about the API and BC-related concerns, I propose to move it out of |
@@ -69,6 +69,14 @@ public function exec($statement); | |||
/** | |||
* Returns the ID of the last inserted row or sequence value. | |||
* | |||
* If no sequence name is given, "0" is returned if there currently is no last insert ID available | |||
* in the current session or in case of an error or if the driver does not support this operation. |
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.
@Majkl578 this is where the error suppression is explicitly defined. It looks dangerous to me that the return value may be different from what's expected because of a runtime issue.
@morozov agreed, pushing this back into |
… with a hardcoded value
…handles per test)
…rever holes have been forgotten
9fa28d2
to
007c327
Compare
007c327
to
6894505
Compare
I'm closing this as "won't fix" and not a bug. Below is the explanation of what exactly was attempted to get fixed and why it shouldn't. Inconsistency in when the last inserted ID is available across platformsThis is what the largest part of the code change is about including the introduction of the This issue is being fixed at the cost of creating either unnecessary coupling between the Instead of fixing this issue, we can explicitly recommend fetching the last insert ID immediately after Inconsistency in what the last inserted ID looks likeYes, there is an inconsistency between what an empty ID looks like (all kinds of empty values) and the non-empty ones (integer or a string). But it's natural for a weakly-typed language like PHP.
Depite the fact that IDs are mostly integers in the DB internals, we cannot represent them as such due to potential overflow issues. At the same time, declaring them strings on the API level is misleading in the way that they are not just strings (e.g. Technically, we can re-iterate on the type consistency later, after #3005 is resolved but besides being theoretically correct, I don't see a practical value in it. |
Reopened #2765 from the personal repo (replaces #2648).
TODO
string
, not anint
? Because on a 32-bit system, a numeric ID will be represented as a string by the driver (e.g.mysqli
). In a general case, an extremely large ID will not fit the int range anyways.NULL
? Because some drivers return0
when there's no ID. Besides that, for the caller, there's no difference between0
andNULL
, they both mean "none".