-
-
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
DBAL 3.0: suggestions to improve the Driver\Connection interface #3335
Conversation
@BenMorel the patch looks reasonable code-wise. Are you planning on fixing failures on Oracle? I'd like to get this in before I finish my project-wide type changes for The IBM DB2 seems caused by not having #3320 in |
@morozov Oracle fixed in 76fd95f. The first failure was that the test expected self::assertSame(1, $this->driverConnection->lastInsertId($sequence)); The second failure, however, expected self::assertFalse($this->connection->lastInsertId(null)); If fixed it with |
Makes sense.
It should be an exception. In fact, previously dbal/lib/Doctrine/DBAL/Driver/OCI8/OCI8Connection.php Lines 142 to 156 in 5da6be5
Please rebase on the latest |
76fd95f
to
9c9c992
Compare
OK, which kind of exception should it throw? A
Fully agree. Should we actually plan to use
✔️ This one is fixed.
About PHPCS failures, unfortunately it does not seem to recognize the existing
Is there a way to make it understand |
I meant a due-dilligence visual check. to prevent new inconsistencies. We'll introduce
|
Handled in 1a0ff77.
Handled in 1cf6e2f. I added a
It doesn't, though:
/**
* {@inheritDoc}
*/
public function lastInsertId(string $name = null) : string Its parent method: /**
* {@inheritdoc}
*/
public function lastInsertId(string $name = null) : string Implemented interface method: /**
* Returns the ID of the last inserted row or sequence value.
*
* @param string|null $name
*
* @return string
*/
public function lastInsertId(string $name = null) : string; Not sure if the problem therefore comes from a wrong case ( |
It means that the parameter should be declared as |
Ah, gotcha. I got somehow convinced that this error was related to improper documentation. Fixed 👍 In Oh, and I explicitly added This is a first step towards my suggestion regarding exception handling in #3334. Anyway, I'm done on this PR now, do you see anything else? |
You're misinterpreting the standard. The standard forbids useless annotations which only contain the variable type which is already specified in the method signature and not containing a description. Meaningful descriptions are more than welcome. |
I got that, and I had 2 choices: removing the annotation, or adding a description to it. I chose the second option! |
@morozov Do you see any other issue with this PR? |
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 more I deal with Connection::lastInsertId()
(see #3074), the less I like it. I think a significant part of the confusion comes from the fact that the calls with and without the $name
are targeted at the same method but have totally different semantics (named sequences and identity columns). This confusing API was originally borrowed from PDO but we don't have to follow it strictrly.
What if you drop Connection::lastInsertId()
from this pull request so that we can focus on what's already clear?
$result = sasql_insert_id($this->connection); | ||
|
||
if ($result === false) { | ||
throw new SQLAnywhereException('The connection is not valid.'); |
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 connection is established in the constructor. If there's a run-time error, it shouldn't be hidden but instead reflected in the exception.
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.
From the docs, it looks like this method can only return an error (false
) "if the $conn is not valid". So IMO the error message should just reflect that (for example if the connection was closed in the meantime).
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.
An invalid connection may have many reasons behind it. If it was valid at the construction time but isn't anymore, I want to know the exact reason (should be available via SQLAnywhereException::fromSQLAnywhereError($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.
I didn't know about this method, fixed!
You're right about I can definitely remove
From what I could see by quickly browsing the (quite numerous) changes, it looks like you're attempting to retain the last insert ID, even if there were other statements executed in between. It's a feature that could create more problems than it would solve, IMHO: As I said in a comment above, whenever I used What if today I have an Again, the more I think about it, the more I think this is an exceptional situation: either you get an ID from the very last statement executed, or you get an exception if it didn't yield an ID. What do you think? |
It's not the point. I mentioned that PR to show that this
I agree. I thought about it some more and also came to the conclusion that you shouldn't want just to look if there's a last inserted ID, you expect it to be there in a certain situation and be able to use it.
You'll need to add more tests next to
|
8559397
to
8f8c235
Compare
dd27900
to
5bf41d2
Compare
if (static::class === self::class) { | ||
// WIP regarding exceptions, see: | ||
// https://github.com/doctrine/dbal/pull/3335#discussion_r234381175 | ||
return new class($message) extends AbstractDriverException { |
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 had to resort to this nasty code, to be able to call the factory method on the abstract class, as I can't use a factory method on an anonymous class.
Exceptions refactoring is really needed 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.
Note that Scrutinizer wrongly reports that static::class === self::class
is always true, I'll file a bug with them.
|
Yes. I only suggested it to not hold the rest of the changes. |
So apart from the type-cast on SQLAnywhere, that we don't know how to test (it doesn't hurt to leave it, as for sure it cannot create a bug, whereas removing it could create one), I think I'm done on this PR, @morozov! Please let me know if you see anything else; I'd like to move forward with splitting |
{ | ||
$message = 'No identity value was generated by the last statement.'; | ||
|
||
if (static::class === self::class) { |
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.
Is this method supposed to be called on the abstract class? I'd rather remove this block since it's a temporary solution than complicate it that much.
Another thought I had in mind is, do we really need driver-specific exceptions which are not originated in the underlying drivers? The primary reason for having OCI8Exception
, MysqliException
, etc. separated out is that different drivers provide different information about errors and different APIs for obtaining this information.
In the case of exceptions like this which originate in the DBAL, it's not applicable. The information is always the same.
Unlike most other use cases, where this separation is useful when a client is interested in catching only a specific type of exceptions, DBAL clients are not interested in driver-specific exceptions because the driver is abstracted out.
Another point is, an exception originated in DBAL, cannot properly implement Doctrine\DBAL\Driver\DriverException
because it doesn't have error code and SQLSTATE by definition.
Can we move these new methods to Doctrine\DBAL\DBALException
(even if it's an API change) and find a better place later?
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.
Is this method supposed to be called on the abstract class? I'd rather remove this block since it's a temporary solution than complicate it that much.
Yes, this is to allow calling AbstractDriverException::noInsertId()
. There is no other solution to factorize the noInsertId()
method as you requested it, without either making AbstractDriverException
not abstract, or creating more driver-specific exception classes.
Another thought I had in mind is, do we really need driver-specific exceptions which are not originated in the underlying drivers? (...) Unlike most other use cases, where this separation is useful when a client is interested in catching only a specific type of exceptions, DBAL clients are not interested in driver-specific exceptions because the driver is abstracted out.
Exactly. That's why I suggested earlier that DriverException
might not be an interface, but a concrete class.
I get your point that subclasses allow for driver-specific factory methods such as fromSqlSrvErrors()
, fromPDOException()
and so on, but they provide nothing that could not be implemented in the Driver\Connection
classes.
For example, in SqlSrvConnection
: throw SQLSrvException::fromSqlSrvErrors()
could be replaced with throw $this->createExceptionFromSqlSrvErrors()
, that would create a generic DriverException
. This removes the need from an SQLSrvException
class altogether.
Another point is, an exception originated in DBAL, cannot properly implement Doctrine\DBAL\Driver\DriverException because it doesn't have error code and SQLSTATE by definition.
Error code and SQLSTATE are currently optional in DriverException
, but anyway I'm not sure why you would throw a DriverException from another place of the DBAL?
As I see it, the driver is the lowest layer of DBAL, and the rest of the DBAL is another layer built on top of it: it uses drivers and handles DriverExceptions, but never throws them.
Can we move these new methods to Doctrine\DBAL\DBALException (even if it's an API change) and find a better place later?
DBALException
does not implement DriverException
, so this would not respect the contract!
What I would suggest, if you see no other issue here, is that we merge this PR as is: it provides well encapsulated changes, and has just a handful of well-documented exotic ways to throw exceptions; as soon as this is merged, I can file another PR to propose to change interface DriverException
to class DriverException
, and get rid of the driver-specific exceptions. This should be a simple change that will clean up the codebase, and will fix all the temporary hacks introduced here. And we can start a fresh discussion there if issues arise.
What do you think?
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.
For example, in
SqlSrvConnection
:throw SQLSrvException::fromSqlSrvErrors()
could be replaced withthrow $this->createExceptionFromSqlSrvErrors()
, that would create a genericDriverException
. This removes the need from anSQLSrvException
class altogether.
This way, you could throw these exceptions only from connections. They should be also available to statements. That's why they are separate classes.
DBALException
does not implementDriverException
, so this would not respect the contract!
What contract? Connection::lastInsertId()
doesn't declare any thrown exception. Throwing an exception (which can happen even in master) is already vilation of 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.
This way, you could throw these exceptions only from connections. They should be also available to statements. That's why they are separate classes.
No problem if implemented as a static method on the Connection, that can be used by the Statement as well.
What contract? Connection::lastInsertId() doesn't declare any thrown exception.
It does now, that's the whole point of what we've been discussing so far, and the contract that the newly introduced tests enforce.
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've pushed a commit in another branch, that I will move to a PR once/if this is merged:
BenMorel@f9e0c5a
It does exactly what I suggested:
AbstractDriverException
is now a concreteDriverException
, replacing the interface- All sub-exceptions are now gone
- All exotic
new class ... extends AbstractDriverException
are now replaced withnew DriverException
- Factory methods from sub-exceptions are moved to a
public static
method in the Connection, that can be used from within the Connection itself and from the Statement - All relevant Connection methods are now explicitly documented as throwing
DriverException
This last point is very important: so far, most of the methods had no documented exceptions, while query()
and exec()
were documented as throwing DBALException
, a contract that was never respected:
PDOConnection::query()
andexec()
throwDriver\PDOException
, which is aDriverException
, not aDBALException
DB2Exception::query()
andexec()
throwDB2Exception
, which is aDriverException
SQLSrvException::query()
andexec()
throwSQLSrvException
(viaSQLSrvStatement::execute()
), which is aDriverException
- ... and so on.
This future PR will make these things right.
The PR cannot be merged in its current state. If it requires extra rework of exceptions, it should be done first, not the other way around. |
I've updated #3367 to be a standalone PR, not based on this one. Some things are intrinsically related, though: for now methods such as This may feel weird, but reflects the current reality: some drivers such as DB2 already throw exceptions here, instead of respecting the current contract. As described in the PR description, that PR is aimed to "make things right" regarding DriverExceptions; once/if merged, the present PR will finish the job by removing the If you think that changes in both PRs are too intrisically related, I can also merge the 2 PRs, but this will be a bigger change, harder to review. |
3850154
to
4fbe91a
Compare
a640b82
to
e7b6c16
Compare
Summary
This PR proposes a first batch of changes to the
Driver\Connection
interface, suggested in #3334:Return
void
inbeginTransaction()
,commit()
,rollBack()
Takeaway from the issue:
Type-hint parameters and return types
beginTransaction()
,commit()
,rollBack()
as described abovelastInsertId()
that always receives astring|null
and always returnsstring
quote()
that should always returnstring
or throw an exception (?)These changes do not break any tests.