Skip to content
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

Support IS NULL checking in Connection#delete() and Connection#update() generated criteria, allowing for null column searches #2688

Merged
merged 3 commits into from
Jun 23, 2017

Conversation

jnvsor
Copy link
Contributor

@jnvsor jnvsor commented Mar 25, 2017

If you pass a null to the where array of update or delete it's
pretty obvious what you want.

Most (all?) databases won't match anything if you actually try
to compare with NULL making this a silent failure without it.

@jnvsor
Copy link
Contributor Author

jnvsor commented Mar 26, 2017

Can anyone tell me what I should do about adding a test? Should I just pick a number that doesn't exist yet and stick it in a @group annotation or is there a protocol to follow here?

@Ocramius
Copy link
Member

Ocramius commented Mar 26, 2017 via email

@jnvsor
Copy link
Contributor Author

jnvsor commented Mar 27, 2017

Thanks! The test is added, so feel free to merge if my points on backwards compatibility are ok

@jnvsor jnvsor changed the title [WIP] Connection: Support IS NULL in update/delete criteria Connection: Support IS NULL in update/delete criteria Mar 27, 2017
$columnList[] = $columnName;
$criteria[] = $columnName . ' = ?';
$paramValues[] = $value;
if (is_null($value)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not duplicate this on the two different methods?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how without passing columnList, criteria and paramValues as references to the helper method. (Which is probably why they're duplicated in the first place) Is that ok?

*/
public function testUpdateWithIsNull()
{
$driverMock = $this->createMock('Doctrine\DBAL\Driver');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the ::class syntax

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copy pasted these from existing test testUpdateWithSameColumnInDataAndIdentifiers - I guess the code style has changed since then? I'll get the changes done

->method('connect')
->will($this->returnValue(new DriverConnectionMock()));

$conn = $this->getMockBuilder('Doctrine\DBAL\Connection')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you extract this to a method (you can also encapsulate $driverMock there) so that it can be reused on delete()?

->will($this->returnValue(new DriverConnectionMock()));

$conn = $this->getMockBuilder('Doctrine\DBAL\Connection')
->setMethods(array('executeUpdate'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the short array syntax


$conn = $this->getMockBuilder('Doctrine\DBAL\Connection')
->setMethods(array('executeUpdate'))
->setConstructorArgs(array(array('platform' => new Mocks\MockPlatform()), $driverMock))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*/
public function testDeleteWithIsNull()
{
$driverMock = $this->createMock('Doctrine\DBAL\Driver');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

->method('connect')
->will($this->returnValue(new DriverConnectionMock()));

$conn = $this->getMockBuilder('Doctrine\DBAL\Connection')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

->will($this->returnValue(new DriverConnectionMock()));

$conn = $this->getMockBuilder('Doctrine\DBAL\Connection')
->setMethods(array('executeUpdate'))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


$conn = $this->getMockBuilder('Doctrine\DBAL\Connection')
->setMethods(array('executeUpdate'))
->setConstructorArgs(array(array('platform' => new Mocks\MockPlatform()), $driverMock))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jnvsor jnvsor force-pushed the isnull branch 2 times, most recently from a2f8ef2 to fc7ff5c Compare March 27, 2017 17:17
Copy link
Member

@deeky666 deeky666 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concerning my comments about the tests, it might be a good idea to add a simple functional test for each operation, too.

* @param array &$criteria Output array of criteria
* @param array &$paramValues Output bound values
*/
protected function setCriteria($identifiers, &$columnList, &$criteria, &$paramValues)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make this method private as it is not meant to be an extension point for derived classes. Also I would suggest naming the method differently as it is not really modifying the state of the object but rather gathering criteria. So renaming it to something like gatherCriteria() would make sense to me.

protected function setCriteria($identifiers, &$columnList, &$criteria, &$paramValues)
{
foreach ($identifiers as $columnName => $value) {
if (is_null($value)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use a null === $value here

foreach ($identifiers as $columnName => $value) {
if (is_null($value)) {
$criteria[] = $this->getDatabasePlatform()->getIsNullExpression($columnName);
} else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid the else here. You can continue early at the end of the if block.

[
'text' => 'string',
'is_edited' => 'null',
'id' => 'null',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifying 'null' type is a bit weird because Doctrine does not have such type. As the testsuite somewhat also serves as API usage documentation I would suggest not promoting misusage here. I am pretty sure that code won't work when actually executed as Doctrine should complain about not knowing null type.
IMO the best would be to not specify any type for null values and let Doctrine decide what to do with it. As an alternative, if you want to complete type specification here, you should use the approriate types that reflect the column type like for id you would probably pass integer as type.

'name' => 'foo',
],
[
'id' => 'null',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above. Also specifying a type for null values here does make even less sense as those values won't be turned into bound parameters.

@jnvsor
Copy link
Contributor Author

jnvsor commented Apr 14, 2017

@deeky666 Addressed your points and added tests to WriteTest. Is there an up to date code style guide somewhere? Lots of the older code doesn't look anything like your suggestions

Copy link
Contributor

@phansys phansys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Status: Needs work.


$data = $this->_conn->fetchColumn('SELECT COUNT(*) FROM write_table WHERE test_int = 30');

$this->assertEquals(1, $data);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, use assertCount() instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assertCount isn't used anywhere in the WriteTest file - are you sure?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, no matter if the right assertion was used in other tests or not, this must not prevent you to make a proper check.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'll use count then. As I asked before:

Is there an up to date code style guide somewhere?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which type of assert choose isn't related to CS, it depends on the type of data you need to check.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but the data was an int. Choosing between doing the count in the assert or in the query is CS

Copy link
Contributor

@phansys phansys left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Status: Needs work.

* @param array &$criteria Output array of criteria
* @param array &$paramValues Output bound values
*/
private function gatherCriteria($identifiers, &$columnList, &$criteria, &$paramValues)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can these arguments be declared?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you mean?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private function gatherCriteria(array $identifiers, array &$columnList, array &$criteria, array &$paramValues)

@jnvsor
Copy link
Contributor Author

jnvsor commented Jun 23, 2017

Any chance I could get a new review?

Copy link
Member

@Ocramius Ocramius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Patch looks awesome, but I will need to go through it to remove that by-ref gatherCriteria signature (returning a triplet instead)

@jnvsor
Copy link
Contributor Author

jnvsor commented Jun 23, 2017

I'll get on that right now then, thanks for the feedback!

@Ocramius
Copy link
Member

@jnvsor ideally, the result would be list($columnList, $criteria, $parameterValues) = $this->theFunction(...)

@jnvsor
Copy link
Contributor Author

jnvsor commented Jun 23, 2017

How should I write up the docstring? Just @returns array [x, y, z]?

@Ocramius
Copy link
Member

@jnvsor

/**
 * ...
 * @return string[][] a triplet with:
 *                    - the first key being the column list
 *                    - the second key being the criteria string
 *                    - the third key being the bound parameter types
 */

@jnvsor jnvsor force-pushed the isnull branch 3 times, most recently from 4087216 to e2e3abc Compare June 23, 2017 20:05
@jnvsor
Copy link
Contributor Author

jnvsor commented Jun 23, 2017

Ok, I've cleaned it up a bit:

    /**
     * Gathers criteria for an update or delete call.
     *
     * @param array $identifiers Input array of columns to values
     * @param array $columns     List of assigned column names
     * @param array $values      List of assigned values
     *
     * @return string[][] a triplet with:
     *                    - the first key being the column names
     *                    - the second key being the values
     *                    - the third key being the criteria strings
     */
    private function gatherCriteria(array $identifiers, array $columns = [], array $values = [])

[...]

list($columnList, $paramValues, $criteria) = $this->gatherCriteria($identifier, $columnList, $paramValues);

How's that?

@Ocramius
Copy link
Member

Why do you need , $columnList, $paramValues);?

@jnvsor
Copy link
Contributor Author

jnvsor commented Jun 23, 2017

Because the SET in an UPDATE query comes before the WHERE - if we don't append to them the parameters will be bound out of order. That said, they're optional because you don't have a SET in a DELETE (only in an UPDATE)

@Ocramius
Copy link
Member

if we don't append

array_merge() the results ;-)

@jnvsor
Copy link
Contributor Author

jnvsor commented Jun 23, 2017

Yeah but then you can't destructure can you? Or should I just array_merge on the other side?

@Ocramius
Copy link
Member

list($columnList, $criteria, $parameterValues) = $this->theFunction(...);
$columns = array_merge($previouslyDefinedColumns, $columnList);
$filter = array_merge($previouslyDefinedCriteria, $criteria);

If you pass a null to the where array of update or delete it's
pretty obvious what you want.

Most (all?) databases won't match anything if you actually try
to compare with NULL making this a silent failure without it.
@jnvsor
Copy link
Contributor Author

jnvsor commented Jun 23, 2017

Done. Though looking at it the variables in update/delete could stand a renaming. columns, values, conditions?

Edit: You know what, never mind. If we start nitpicking variable naming we'll be here another 3 months!

@Ocramius
Copy link
Member

@jnvsor very good improvement! The last bit to improve is to avoid overwriting variables at all, and then we're done. This bit specifically:

$columnList = array_merge($columnList, $whereColumns);
$paramValues = array_merge($paramValues, $whereValues);

Indeed, a variable rename is needed

@jnvsor
Copy link
Contributor Author

jnvsor commented Jun 23, 2017

Added a variable rename and stopped the overwriting

@Ocramius
Copy link
Member

@jnvsor awesome! Waiting for travis and then this can be merged :-)

@Ocramius Ocramius added this to the 2.6 milestone Jun 23, 2017
@Ocramius Ocramius assigned Ocramius and unassigned deeky666 Jun 23, 2017
@Ocramius
Copy link
Member

🚢

@Ocramius Ocramius merged commit 03e1718 into doctrine:master Jun 23, 2017
@Ocramius
Copy link
Member

Thanks @jnvsor!

@Ocramius Ocramius changed the title Connection: Support IS NULL in update/delete criteria Support IS NULL checking in Connection#delete() and Connection#update() generated criteria, allowing for null column searches Jul 22, 2017
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 15, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants