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

Add support for DISTINCT clause #3696

Merged
merged 1 commit into from
Oct 13, 2019
Merged

Add support for DISTINCT clause #3696

merged 1 commit into from
Oct 13, 2019

Conversation

bingo-soft
Copy link
Contributor

@bingo-soft bingo-soft commented Oct 12, 2019

Q A
Type improvement
BC Break no
Fixed issues

Summary

The same functionality as in Doctrine/ORM. New method distinct() in QueryBuilder class and new test testSimpleSelectWithDistinct in QueryBuilderTest are provided. This PR follows up the previous one with extended discussion - #3695

Closes #3340

greg0ire
greg0ire previously approved these changes Oct 12, 2019
@greg0ire
Copy link
Member

Please squash all three commits together

@morozov
Copy link
Member

morozov commented Oct 12, 2019

Looks good but resetQueryPart() needs to be updated to be able to handle boolean parameters.

@bingo-soft
Copy link
Contributor Author

resetQueryPart

I've not pushed it, yet. But it seems, like there is no need to tune resetQueryPart. This is the test case, that is ok:

public function testResetQueryPartWithDistinct() : void
{
    $qb = new QueryBuilder($this->conn);

    $qb->select('u.*')->distinct()->from('users', 'u');

    self::assertEquals('SELECT DISTINCT u.* FROM users u', (string) $qb);
    $qb->resetQueryPart('distinct');
    self::assertEquals('SELECT u.* FROM users u', (string) $qb);
}

BTW. In Doctrine ORM, which also supports distinct method, resetDQLPart does not handle specifically boolean parameters - https://github.com/doctrine/orm/blob/master/lib/Doctrine/ORM/QueryBuilder.php#L1456

@greg0ire
Copy link
Member

I think @morozov is correct. Even though this works, it is unsafe because now the value is null, and when reading the code people will assume it is always a boolean.

BTW. In Doctrine ORM, which also supports distinct method, resetDQLPart does not handle specifically boolean parameters

We might have sinned in the past, let's not invoke consistency and repeat the same mistake. Our lives will be simpler if boolean parameters have 2 values and not 3.

@bingo-soft
Copy link
Contributor Author

bingo-soft commented Oct 13, 2019

I will not insist, bet let me put my two cents in. I PHP context, null means that some variable is unset. And when reading the code people may also assume, that the variable is not true or false, but simply set or unset. And so, it is perfectly fine even for some boolean variable to be unset or null. So, probably we should make sqlParts['distinct'] => null by default? And check if it is not null in getSQLForSelect method? Many other sqlParts, like where or having are also compared to null, even though they are strings. And I think there is nothing special about boolean variable. So, my proposal is to change this

 private $sqlParts = [
    'select'  => [],
    'distinct'  => false,
    'from'    => [],
    ...
]; 

to this:

private $sqlParts = [
    'select'  => [],
    'distinct'  => null,
    'from'    => [],
    ...
]; 

and this

$query = 'SELECT ' . ($this->sqlParts['distinct'] ? 'DISTINCT ' : '') .
              implode(', ', $this->sqlParts['select']);

to this:

$query = 'SELECT ' . ($this->sqlParts['distinct'] !== null ? 'DISTINCT ' : '') .
              implode(', ', $this->sqlParts['select']);

@greg0ire
Copy link
Member

greg0ire commented Oct 13, 2019

And I think there is nothing special about boolean variable.

This is where we disagree. To me, boolean variables are very special: they are part of a finite set of only 2 values: true or false. To me null means something else: it means we don't have the data to make a decision. But in SQL, if DISTINCT is not specified, then it will not be applied. We can easily represent that by using false as the default value for distinct.

If you use bool as a return type for a method, then you have the guarantee you can use it in an if clause without additional type: if ($this->isDistinct()) {, and it reads very nicely. In #3340 , something similar was done, but what's not great about it is that it will not be reset by a call to resetDqlParts()

Instead of handling booleans in a generic way (you don't know if a boolean should default to true or false depending on if it's named $isGuilty or $isInnocent), here is what you could could do in:

    private const SQL_PARTS_DEFAULTS = [
        'select'   => [],
        'distinct' => false,
        'from'     => [],
        'join'     => [],
        'set'      => [],
        'where'    => null,
        'groupBy'  => [],
        'having'   => null,
        'orderBy'  => [],
        'values'   => [],
    ];

    /**
     * The array of SQL parts collected.
     *
     * @var mixed[]
     */
    private $sqlParts = self::SQL_PARTS_DEFAULTS;

    public function resetQueryPart($queryPartName)
    {
        $this->sqlParts[$queryPartName] = self::SQL_PARTS_DEFAULTS[$queryPartName];

        $this->state = self::STATE_DIRTY;

        return $this;
    }

resetQueryPart is nicely simplified :)

@bingo-soft
Copy link
Contributor Author

resetQueryPart is nicely simplified :)

This was one of two ideas I had - 1) switching to null in $sqlParts or 2) creating a constant array. I think, both variants have their pros and cons. Let's see what other reviewers think and make final solution.

@greg0ire
Copy link
Member

What cons do you see with 2) ?

@bingo-soft
Copy link
Contributor Author

What cons do you see with 2) ?

Extra lines of code)

@greg0ire
Copy link
Member

As you can see in my previous answer, it actually means fewer lines of code, doesn't it?

@bingo-soft
Copy link
Contributor Author

bingo-soft commented Oct 13, 2019

As you can see in my previous answer, it actually means fewer lines of code, doesn't it?

Comparing to the current PR, switching to null for distinct in $sqlParts requires no extra lines of code. Your solution looks good, but introduses one new constant.

if ($this->isDistinct()) {

BTW. I'm not sure, that this public or private method is needed. We do not have similar check methods for other sql parts. But of course, I may be wrong. I'm still not sure whether null or false is better as a default value for distinct part of sql.

@greg0ire
Copy link
Member

but introduses one new constant.

Yeah but the new constant is private, so no big deal, and it simplifies both $sqlParts and resetQueryPart()

BTW. I'm not sure, that this public or private method is needed. We do not have similar check methods for other sql parts. But of course, I may be wrong. I'm still not sure whether null or false is better as a default value for distinct part of sql.

I wasn't suggesting you add that method, I think it's fine to use the array directly

greg0ire
greg0ire previously approved these changes Oct 13, 2019
Copy link
Member

@greg0ire greg0ire left a comment

Choose a reason for hiding this comment

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

👌

@bingo-soft
Copy link
Contributor Author

bingo-soft commented Oct 13, 2019

👌

@greg0ire. Should we squash these two commits into one?

Copy link
Member

@morozov morozov left a comment

Choose a reason for hiding this comment

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

👍

@morozov morozov merged commit 7241a65 into doctrine:master Oct 13, 2019
@morozov morozov self-assigned this Oct 13, 2019
@morozov
Copy link
Member

morozov commented Oct 13, 2019

Thank you @bingo-soft and @greg0ire!

@morozov morozov added this to the 2.10.0 milestone Oct 13, 2019
rgrellmann added a commit to Rossmann-IT/dbal that referenced this pull request Apr 22, 2020
# Release v2.10.0

This is a minor release of Doctrine DBAL that aggregates over 70 fixes and improvements developed by 25 contributors over the last year.

This release focuses on internal code quality improvement and deprecating the functionality identified for removal in the next major release.

## Backwards Compatibility Breaks

This release introduces a minor BC break. Default column values are no longer handled as SQL expressions. They are converted to SQL literals (e.g. escaped). The previous behavior was not portable and was never by design.

Clients must now specify default values in their initial form, not in the form of an SQL literal (e.g. escaped).

**Before:**

```php
$column->setDefault('Foo\\\\Bar\\\\Baz');
```

**After:**

```php
$column->setDefault('Foo\\Bar\\Baz');
```

## Deprecations

- The usage of the `getDriver()`, `getDatabasePlatform()` and `getSchemaManager()` methods of the `ConnectionEventArgs` class has been deprecated.
- The usage of the `getDatabasePlatform()` method of the `SchemaColumnDefinitionEventArgs` class has been deprecated.
- The usage of the `getHost()`, `getPort()`, `getUsername()` and `getPassword()` methods of the `Connection` class has been deprecated.
- Passing multiple SQL statements as an array to `SchemaAlterTableAddColumnEventArgs::addSql()` and the same method in other `SchemaEventArgs`-based classes is deprecated.
- Calling `AbstractSchemaManager::tablesExist()` with a string argument is deprecated.
- Calling `OracleSchemaManager::createDatabase()` without an argument or by passing `NULL` is deprecated.
- Unused schema manager methods are deprecated.
   -  `AbstractSchemaManager::_getPortableFunctionsList()`,
   - `AbstractSchemaManager::_getPortableFunctionDefinition()`,
   - `OracleSchemaManager::_getPortableFunctionDefinition()`,
   - `SqliteSchemaManager::_getPortableTableIndexDefinition()`.
- The usage of `NULL` to indicate empty `$username` or `$password` when calling `Doctrine\DBAL\Driver::connect()` is deprecated.
- Method `Doctrine\DBAL\Platforms::_getAlterTableIndexForeignKeySQL()` has been deprecated as no longer used.
- Property `Doctrine\DBAL\Driver\OCI8\OCI8Statement::$_PARAM` has been deprecated as not used.
- Method `Doctrine\DBAL\Driver::getName()` is deprecated.
- The usage of user-provided `PDO` instance is deprecated.
- `Type::*` constants are deprecated.
- The `Doctrine\DBAL\Driver\SQLSrv\SQLSrvStatement::LAST_INSERT_ID_SQL` constant has been deprecated.
- The constants in `Doctrine\DBAL\SQLParserUtils` have been deprecated.
- The `Doctrine\DBAL\Logging\LoggerChain::addLogger` method has been deprecated.

Please see the details in the [UPGRADE.md](UPGRADE.md) documentation.

## New Features and Improvements

- [3674: Add missing MySQL 8.0 reserved keywords](doctrine#3674) thanks to @loilo
- [3512: Support for comments on table in all databases](doctrine#3512) thanks to @moufmouf
- [3418: Add column charset for MySql](doctrine#3418) thanks to @altertable

**MySQL-related changes:**

 - [3668: Quote collation on MySQL](doctrine#3668) thanks to @staudenmeir
 - [3374: Clean up `MySqlPlatform::getListTableIndexesSQL()` fields](doctrine#3374) thanks to @BenMorel
 - [3311: Ensuring correct `ALTER TABLE` statement for creation of an `AUTO INCREMENT` column as new `PRIMARY KEY`](doctrine#3311) thanks to @arnegroskurth

**Driver level changes:**

- [3677: Relax statement type declaration](doctrine#3677) thanks to @greg0ire
- [3521: Maintain platform parameter in connection params](doctrine#3521) thanks to @jwage and @Perf
- [3588: Add commit result bool](doctrine#3588) thanks to @otazniksk

**Schema management:**

- [2960: Handle default values as values, not SQL expressions](doctrine#2960) thanks to @morozov

**Types improvements:**

 - [3356: Extract constants for built-in types from Type to Types](doctrine#3356) thanks to @Majkl578
 - [3354: Extract type factory and registry from Type into TypeRegistry](doctrine#3354) thanks to @Majkl578

**Compatibility with Symfony 5:**

 - [3706: add missing exit codes to ensure Symfony 5 compatibility](doctrine#3706) thanks to @dmaicher
 - [3563: Allow Symfony 5](doctrine#3563) thanks to @nicolas-grekas

**Query Builder:**

 - [3696: Add support for `DISTINCT` clause](doctrine#3696) thanks to @bingo-soft

**Logging:**

 - [3572: Make LoggerChain use constructor to add loggers instead of adder method](doctrine#3572) thanks to @ostrolucky

**Code quality improvements:**

 - [3667: Phpstan fix backport](doctrine#3667) thanks to @morozov
 - [3663: Updated PHPStan to v0.11.15](doctrine#3663) thanks to @morozov
 - [3604: Updated Jetbrains PhpStorm stubs to 2019.1](doctrine#3604) thanks to @morozov
 - [3549: Removed the assertion which doesn't work with a user-provided PDO connection](doctrine#3549) thanks to @morozov
 - [3489: Update doctrine coding standard from 5.0 to 6.0](doctrine#3489) thanks to @amaabdou
 - [3481: Updated PHPStan to v0.11.3](doctrine#3481) thanks to @morozov
 - [3443: PHPStan Level 7](doctrine#3443) thanks to @morozov
 - [3442: PHPStan Level 6](doctrine#3442) thanks to @morozov
 - [3436: PHPStan Level 5](doctrine#3436) thanks to @morozov
 - [3435: PHPStan Level 4](doctrine#3435) thanks to @morozov
 - [3432: Updated PHPStan to v0.11](doctrine#3432) thanks to @morozov

**Test suite improvements:**

 - [3705: Don't skip a test for sqlite](doctrine#3705) thanks to @Federkun
 - [3689: Updated PHPUnit to 8.4.1](doctrine#3689) thanks to @morozov
 - [3664: Refactor usage of MockBuilder's deprecated setMethods()](doctrine#3664) thanks to @baspeeters
 - [3643: Bumped PHPUnit requrement to ^8.3.3, removed dependency on symfony/phpunit-bridge](doctrine#3643) thanks to @morozov
 - [3609: Reworked the mocks generated by Prophecy using PHPUnit](doctrine#3609) thanks to @morozov
 - [3608: Added a unit test for Doctrine\DBAL\Logging\LoggerChain](doctrine#3608) thanks to @morozov
 - [3600: Updated PHPUnit to 8.2.1](doctrine#3600) thanks to @morozov
 - [3575: Enforced parameter and return value types in test classes](doctrine#3575) thanks to @morozov
 - [3566: Upgraded to PHPUnit 8.1.6 and reworked the remaining driver exception mock](doctrine#3566) thanks to @morozov
 - [3555: Removed the rest of mock classes](doctrine#3555) thanks to @morozov
 - [3546: Reworked driver exception tests](doctrine#3546) thanks to @morozov
 - [3530: Improve ExceptionTest::testConnectionExceptionSqLite](doctrine#3530) thanks to @jwage
 - [3474: Remove more hard-coded mock classes](doctrine#3474) thanks to @morozov
 - [3470: Replaced MockPlatform with the ones generated by PHPUnit](doctrine#3470) thanks to @morozov
 - [3468: Marked some test classes abstract](doctrine#3468) thanks to @morozov
 - [3446: Upgraded PHPUnit to 8.0](doctrine#3446) thanks to @morozov

**Documentation improvements:**

 - [3616: Fix typo in docblock](doctrine#3616) thanks to @rdarcy1
 - [3559: add .github/FUNDING.yml](doctrine#3559) thanks to @jwage
 - [3556: Removed 2.8 from README](doctrine#3556) thanks to @morozov
 - [3514: Expand list of keywords in composer.json](doctrine#3514) thanks to @Majkl578
 - [3504: fix doctrine#3479 (typos in example-code in QueryBuilder)](doctrine#3504) thanks to @DavidBruchmann
 - [3503: Fix the branch alias for master](doctrine#3503) thanks to @stof
 - [3463: fixed a typo in PoolingShardConnection phpdoc](doctrine#3463) thanks to @adapik
 - [3408: Removed unused build files](doctrine#3408) thanks to @morozov
 - [3404: Link to Slack instead of Gitter](doctrine#3404) thanks to @greg0ire
 - [3376: Bump version to 2.10.0-DEV](doctrine#3376) thanks to @morozov

**CI improvements:**

 - [3688: Temporarily disable the usage of PHPUnit 8.4 due to a regression](doctrine#3688) thanks to @morozov
 - [3654: fix Appveyor builds](doctrine#3654) thanks to @mmucklo
 - [3644: Using command line options to configure MySQL 8 instead of mounting a config file](doctrine#3644) thanks to @morozov
 - [3509: Enabled the build against IBM DB2 on PHP 7.3 on Travis CI](doctrine#3509) thanks to @morozov
 - [3528: Reworked SQL Server installer on Travis CI](doctrine#3528) thanks to @morozov
 - [3484: Use latest stable versions of sqlsrv and pdo&doctrine#95;sqlsrv on CI](doctrine#3484) thanks to @morozov
 - [3475: Make PHP 7.3 the primary PHP version on Travis CI](doctrine#3475) thanks to @morozov
 - [3473: Avoid database connection from PHPUnit data providers](doctrine#3473) thanks to @morozov
 - [3469: Replaced Xdebug with PCOV for code coverage](doctrine#3469) thanks to @morozov
 - [3405: Travis CI: PHP 7.3 tests on MySQL 8.0](doctrine#3405) thanks to @BenMorel
 - [3394: Grouped PHPStan and PHP&doctrine#95;CodeSniffer for parallel execution](doctrine#3394) thanks to @morozov
 - [3388: Require PHP 7.2, drop <7.2 in Composer & on CI](doctrine#3388) thanks to @morozov
 - [3122: Introduced a smoke testing phase on Travis to run SQLite tests and CS checks first](doctrine#3122) thanks to @morozov

**Deprecations:**

 - [3708: New deprecations for 2.10](doctrine#3708) thanks to @morozov and @jwage
 - [3598: Deprecated some unused code bits](doctrine#3598) thanks to @morozov
 - [3558: Deprecated Driver::getName()](doctrine#3558) thanks to @morozov
 - [3554: The usage of user-provided PDO instance is deprecated](doctrine#3554) thanks to @morozov
 - [3542: Deprecate SQLSrvStatement::LAST&doctrine#95;INSERT&doctrine#95;ID&doctrine#95;SQL constant.](doctrine#3542) thanks to @jwage
 - [3541: Deprecate the public constants in SQLParserUtils](doctrine#3541) thanks to @jwage

# gpg: Signature made Sun Nov  3 17:56:11 2019
# gpg:                using RSA key 374EADAF543AE995
# gpg: Can't check signature: public key not found

# Conflicts:
#	.travis.yml
#	README.md
#	lib/Doctrine/DBAL/Platforms/SQLAnywherePlatform.php
#	lib/Doctrine/DBAL/Schema/MySqlSchemaManager.php
#	lib/Doctrine/DBAL/Schema/Table.php
#	lib/Doctrine/DBAL/Version.php
#	phpcs.xml.dist
#	tests/Doctrine/Tests/DBAL/Functional/ConnectionTest.php
#	tests/Doctrine/Tests/DBAL/Functional/ExceptionTest.php
#	tests/Doctrine/Tests/DBAL/Functional/MasterSlaveConnectionTest.php
#	tests/Doctrine/Tests/DBAL/Functional/Platform/DefaultExpressionTest.php
#	tests/Doctrine/Tests/DBAL/Functional/ResultCacheTest.php
#	tests/Doctrine/Tests/DBAL/Platforms/SQLAnywherePlatformTest.php
#	tests/Doctrine/Tests/DBAL/Schema/ComparatorTest.php
#	tests/travis/mysql.docker.travis.xml
#	tests/travis/mysqli.docker.travis.xml
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 7, 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.

3 participants