Problem/Motivation
Drupal\Core\Database\Connection class has a custom prepare method:
public function prepare($statement, array $driver_options = array()) {
return $this->connection->prepare($statement, $driver_options);
}
But in prepareQuery it is directly calling the PDO prepare instead of it's custom implementation:
public function prepareQuery($query) {
$query = $this->prefixTables($query);
return $this->connection->prepare($query);
}
This means that statements are never prepared with the driver options! Also custom database drivers are unable to implement custom Statement classes without overriding prepareQuery to fix this issue.
Proposed resolution
Deprecate Drupal\Core\Database\Connection::prepare()
and Drupal\Core\Database\Connection::prepareQuery()
.
Create a new method Drupal\Core\Database\Connection::prepareStatement()
to replace the two deprecated methods. The method takes two parameters: $query
and $options
. The $query
holds the string version of the SQL query and $options
holds an array options with which the query should be executed.
Remaining tasks
TBD
User interface changes
None
API changes
As described in the proposed solution and the CR.
Comment | File | Size | Author |
---|---|---|---|
#84 | 2345451-84.patch | 16.54 KB | mondrake |
#84 | interdiff_83-84.txt | 644 bytes | mondrake |
Comments
Comment #2
markdorisonComment #3
daffie CreditAttribution: daffie commentedI think this is a bug and I agree with the solution. What we also should do is to remove the method prepareQuery from the SQLite driver, because that method does already has the fix and the only difference with its parent method is the fix.
For testing: I do not see how we can create tests for this.
@david_garcia: A very good find and thank you!
Comment #4
david_garcia CreditAttribution: david_garcia commented@daffie Removed the workaround in the SQLite driver. I also had this workaround in the MS SQL Server driver.
Comment #5
daffie CreditAttribution: daffie commentedThis problem is a bug and I agree with the solution.
The patch looks good to me.
For testing: I do not see how we can create tests for this.
For me the patch is RTBC.
Comment #6
xjmWe should be able to add unit tests, no?
This bugfix seems like it might be backportable, but I wasn't sure. The impact on custom drivers would be that they now get tables prefixed, and then use their own overridden version of the prepare method if they don't already. What impact would this have (if any) on the MySQL and PostgreSQL drivers?
Here is the method we would now be calling:
https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Database%...
A bit in the docblock there confused me:
That's copied from the PDO method:
http://php.net/manual/en/pdo.prepare.php
But in any case, unit tests would be good. :)
Comment #7
daffie CreditAttribution: daffie commentedAdded test to check if the method prepareQuery calls the method prepare.
Comment #8
daffie CreditAttribution: daffie commentedForgot the test only patch to show that it fails without the fix.
Comment #10
daffie CreditAttribution: daffie commentedThe testbot fails for the patch with the fix are not related to the patch.
The testbot fails for the patch without the fix.
The patch is ready for a good review.
Comment #11
david_garcia CreditAttribution: david_garcia commentedLooks good.
Comment #12
alexpottFor rtbc re-testing to work the last patch on the issue has to be the rtbc patch. Also the patch in #7 does not look ready from the test results.
Comment #13
daffie CreditAttribution: daffie commentedAll testbot problems where not related to the patch. Now all testbot results are green. So back to RTBC.
Comment #14
alexpottDo we need a followup for this? The issue title does not seem to chime with the actually patch either. Also the last patch on the issue is still not the rtbc patch.
Comment #15
daffie CreditAttribution: daffie commented@alexpott: Totally forgot to add the PDO options. The patch from comment #8 is the same as that from comment #7 only without the fix and that one should be failing. Which it does.
Comment #17
daffie CreditAttribution: daffie commentedThe testbot failures are related to the patch with the test and without the fix and that one should fail. So read for review.
Comment #24
BeakerboyIs this something that still needs to be worked on?
Comment #25
daffie CreditAttribution: daffie commentedYes, this is something that should be fixed.
Comment #26
sokru CreditAttribution: sokru as a volunteer commentedReroll from #15
Comment #28
BeakerboyPatch 26 passes the
Connection::defaultOptions()
array to the PDO object. Is this okay?Connection::defaultOptions()
is not an array of PDO driver settings. It's things like'throw_exception' => TRUE
that the Drupal Connection uses. Is PDO smart enough to know that'throw_exception'
is not a valid option and silently ignore it? (testing shows it must). I've been playing around with this and SQL Server does not seem to like something about this process.If we were to follow the guidance of the docblock, and only pass PDO options to
Connection::prepare()
, then there needs to be some place to translate the contents ofConnection::defaultOptions()
and create an array of PDO driver settings. It looks like prepareQuery() is supposed to be the place, so I think the call should be something like:Drivers that wish to pass some default options will have to override
Connection::prepareQuery()
and specify them. Similarly, there needs to be a way for Connection to prepare a single statement with a different set of options (and PDO driver settings) without changing the global state of the entire Connection. Something like:Alternatively, the existing
Connection::prepareQuery()
could accept an array of options as specified above. This is what sqlsrv currently does, but the method signature of prepareQuery changes in Drupal 9, so I have to refactor the way this was implemented in the Drupal 8 driver.My vote would be to change it to this:
As an aside...where, exactly, does this supposed "caching of prepared statements" occur that is mentioned in the docbock? Does PDO cache queries? Is this consistent with all databases? Is there a setting which affects this behavior?
Comment #29
daffie CreditAttribution: daffie commented@Beakerboy: I do not fully understand what you want to happen. I understand that SQL server has some problems with "extra/other" connection options. Could you write a patch?
Comment #30
BeakerboyThe return type listed in Connection::prepare() is listed as @return \PDOStatement|false while the type in Connection::prepareQuery() is @return \Drupal\Core\Database\StatementInterface despite the fact that they both call $this->connection->prepare(). The comment block for $this->connection says it is of type \PDO.
There probably needs to be some clarification as to the differences and purposes of prepare() vs prepareQuery(). Is Connection::prepare() only to be used when the statement class is not provided by Drupal? As far as I can see, Connection::prepare() is never used. I think understanding the intention of having two preparation methods is key to solving this issue.
Comment #31
alexpottI think this is a legacy impact of D7 architecture. In Drupal 7 the connection class extends directly from \PDO - so in order have a different prepare we added prepareQuery() - so now we have both prepare and prepareQuery(). Then in Drupal 8 we disentangle PDO and our Connection object and to preserve BC we implement the prepare method.
I think the best steps forward here are to:
Comment #32
alexpottAlso let's rename $driver_options to $pdo_driver_options to be 100% clear what they are. Note the only real use cases for passing stuff in here is custom database driver dependent code. It's not stuff we should see in core or normal contrib.
Comment #33
BeakerboyWhat drupal version should the deprecation be targeted for?
Comment #34
alexpottDrupal 10. We can only make this change / tidy up in Drupal 9.1.x
Comment #35
mondrake+1 to #31.
If I may, when touching this, I would also suggest to pass in the $args variable (see
::query
). That could help contrib to manage e.g. replacing at low level named placeholders with positional placeholders and allow running with database clients that do not support named placeholders (for example, mysqli instead of pdo_mysql).In my experimental DruDbal driver, I have the following in the
Connection
class:then, in the
Statement
constructor,This also tells me that maybe instead of a method on the Connection class to retrieve a (Drupal) StatementInterface object, we might as well just deprecate them all and just construct an instance of the driver's Statement object and move the initialization code there.
Comment #36
mondrakeVery rough proposal re. #35 - no interdiff, different approach.
Comment #37
mondrakeTweaks.
Comment #38
mondrakeComment #39
mondrakeOK, I'll work on a test issue and report back when I have sth shareable.
Comment #40
shobhit_juyal CreditAttribution: shobhit_juyal at Srijan | A Material+ Company commentedI've read the lastest patch and found some typos in the annotation. Therefore, I found it useful to provide the patch for those fixes here ( as per the code revewing guidelines).
Comment #41
alexpott@mondrake looking at #38
I think we should only be deprecating prepare() and I think we need to add the $pdo_driver_options to this method.
Comment #42
Beakerboy@shobhit_juyal Your formatting issue is unrelated to this one. As straightforward as your patch is, it’s likely to be RTBC and merged much more quickly on its own than to wait for this issue to come to consensus and for someone to merge your formatting proposal with it.
Comment #43
mondrakeOnly deprecating prepare() as per #41. Still, adding also the $args to prepareQuery per #35, which would fit with DruDbal.
Comment #45
mondrakePgSql fixes.
Comment #46
mondrakeAdd passing the options to the PDO prepare() method ... which is actually the reason of this issue.
Comment #47
daffie CreditAttribution: daffie commentedPatch looks good!
We are touching this method. Can we add an @inheritdoc docblock to the method?
Can we add a test that uses the pdo_driver_options.
The IS needs an update.
A CR is needed.
Comment #48
mondrakeActually, here
$options
does not bear PDO options, but the options passed to::query
. Passing those as such to the prepare statement would lead to unpredictable results. I think we should pass a$options['pdo']
subarray, documenting clearly in::query
docblock that when you want to pass PDO options to the driver, you have to structure thos likewise. I do not think core will be using any of this, right?Comment #49
mondrakeAlso, I would suggest to deprecate
prepareQuery
so we can rename the method toprepareStatement
, since that's what this method is doing AFAICS.EDIT - by the way this would also be more gentle to contrib, that would be otherwise forced to update the signature of prepareQuery.
Comment #50
daffie CreditAttribution: daffie commentedprepareQuery
toprepareStatement
.Why are we adding this parameter to the method prepareQuery?
Comment #51
mondrake#50.3 see #35.
Mainly for contrib.
btw, StatementPrefetch, used by Sqlite, requires the args.
Comment #52
alexpottGiven that this is not used in the default implementation we need a couple of things.
* Good docs about why it's here.
* A test driver that helps us test was is passed in.
$options here are not pdo options. I don't think we should be passing them in here.
Comment #53
mondrakeSo here:
Comment #54
mondrakeComment #55
BeakerboyThe doc block says
Is that true? Where? How?
Comment #56
mondrakeWell I think that means that if, instead of calling
query
, you callqueryPrepare
and then iterate on the statement withexecute
and different sets of args, you just ‘reuse’ the single initial statement object. But I am not sure there is explicit test coverage for that, and AFAICS it’s not a pattern used in core.Comment #57
mondrakeFixes for PgSql, deprecation test for prepareQuery, and adding a test for prepareStatement exceptions like prepare used to have. Need a test for prepareStatement (in general + for the specifics of the $args and $options parameters). There is no test coverage in HEAD currently for prepareQuery :( otherwise we would have got deprecation errors.
Comment #58
mondrakeFixing failures + adding test coverage for prepareStatement. Still missing a test for usage of $args and $options within an alternative test driver.
Comment #59
alexpottNow we have prepare and prepareQuery and prepareStatement. Yes two are deprecated but still... I'm not entirely convinced that passing more than $quote_identifiers and $pdo_driver_options is a good idea. I'm not sure that we really want DB drivers to execute any real logic in this method.
This is a good idea. Adding this as an additional option here makes a ton of sense.
Comment #60
mondrakeAddressing SQLite failure. Thinking on a test to demonstrate #35.
Comment #61
mondrakeComment #62
BeakerboyOne thing to consider is, if a driver overrides the Statement class and wishes to pass statement-level, non PDO direction to the class, there needs to be a place for this. I think this is a good argument for not mandating that only PDO options are passed to prepareQuery (or prepareStatement).
Comment #63
daffie CreditAttribution: daffie commentedHi @mondrake: As for your DruDbal database driver and the
$args
parameter in the methodDrupal\Core\Database\Connection::prepareStatement()
. I think that we should remove the$args
parameter inDrupal\Core\Database\Connection::prepareStatement()
as it is not used in core and the method is also not used outside of any database driver. Therefor your DruDbal driver can create it own version of the method with the extra parameter$args
. If this does not work for the DruDbal database driver, then please say so and explain why it does not. Thank you for working on this patch, it looks good!Comment #64
mondrake#63 yes I think I can work with that.
Comment #65
mondrakeOn this.
Comment #66
mondrakeRemoved $args.
Comment #67
mondrakeThe reason I was reluctant to remove
$args
is that if you use::prepareStatement
outside of::query
, or more in general outside of database driver code, and your driver Statement object needs that at the time of instantiating it, then there will be no way to get that back. In my specific case in DruDbal, since there's a case (as described in #35, already) that I will need to alter the querystring BEFORE it is prepared into the statement, I will change my implementation of Statement just to hold calling the lower-level statement prepare method until I have the $args available, i.e. until the first time::execute
is called.Comment #68
alexpottIt's quite tempting to make the \Drupal\Core\Database\Connection implementations call prepareStatement(). That way every core driver has the same deprecations and any prepareStatement overrides are where to look for anything special.
Plus the deprecated methods need an @deprecated annotation.
If instead of a try catch can we use $this->expectException - and change the class dependent on the db driver.
Comment #69
mondrakeThanks for review.
#68.1: Done, with the caveat that
prepareQuery
, but not onprepare
unless we accept a behavior change since that method is not resolving the table names nor replacing the square brackets with identifiers#68.2: actually that entire test seems weird and maybe a bit rusty - I assume that, with StatementPrefetch, SQLite syntax is no longer checked at prepare time? Let's see this.
Comment #70
mondrakeComment #71
daffie CreditAttribution: daffie commentedCreated a change record.
Comment #72
daffie CreditAttribution: daffie commentedThis method is not being replaced by ::prepareStatement. It is being replaced by \PDO::prepare().
Can we change this to: "@see https://www.php.net/manual/en/pdo.prepare.php"
Comment #73
mondrakeComment #74
mondrakeThanks @daffie!
#72:
1. well yes and no - I added more explanation to the deprecation message.
2. Done
3. Done
Comment #75
mondrakeComment #76
daffie CreditAttribution: daffie commentedComment #77
daffie CreditAttribution: daffie commentedUpdated the IS.
Comment #78
daffie CreditAttribution: daffie commentedAll code changes look good to me.
The IS has been updated.
The 2 deprecated method have deprecation testing.
There is also testing for the new method.
There are a lot of comments added to the patch.
For me it is RTBC.
Comment #79
mondrakeI filed a possible follow-up, #3137883: Deprecate passing a StatementInterface object to Connection::query. Looking forward to discussion there!
Comment #80
shobhit_juyal CreditAttribution: shobhit_juyal at Srijan | A Material+ Company commentedComment #81
Gábor HojtsyThe current relevant tag would have been "Drupal 9 porting weekend" but there was no activity on it yet for that, so removing it for now.
Comment #83
mondrake#74 got broken in test by #3133798: Semicolon removed from query even when it is allowed. Here we are introducing return typehints for
prepareStatement
which is a benefit, I suppose.Comment #84
mondrakeComment #85
daffie CreditAttribution: daffie commentedTest fix looks correct.
Back to RTBC.
Comment #86
daffie CreditAttribution: daffie commentedComment #87
alexpottCommitted 3917030 and pushed to 9.1.x. Thanks!
As this contains deprecations we can only commit this to 9.1.x which might mean that some contrib db drivers will only support 9.1.x and on but there's not much we can do about that.