diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index f8094d6b3d..1b78760e97 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -4,6 +4,7 @@ Yii Framework 2 Change Log 2.0.14 under development ------------------------ +- Enh #15047: `yii\db\Query::select()` and `yii\db\Query::addSelect()` now check for duplicate column names (wapmorgan) - Enh #14643: Added `yii\web\ErrorAction::$layout` property to conveniently set layout from error action config (swods, cebe, samdark) - Enh #13465: Added `yii\helpers\FileHelper::findDirectory()` method (ArsSirek, developeruz) - Enh #8527: Added `yii\i18n\Locale` component having `getCurrencySymbol()` method (amarox, samdark) diff --git a/framework/db/Query.php b/framework/db/Query.php index cec4d36497..896709e465 100644 --- a/framework/db/Query.php +++ b/framework/db/Query.php @@ -594,7 +594,7 @@ PATTERN; } elseif (!is_array($columns)) { $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); } - $this->select = $columns; + $this->select = $this->getUniqueColumns($columns); $this->selectOption = $option; return $this; } @@ -621,6 +621,7 @@ PATTERN; } elseif (!is_array($columns)) { $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); } + $columns = $this->getUniqueColumns($columns); if ($this->select === null) { $this->select = $columns; } else { @@ -630,6 +631,51 @@ PATTERN; return $this; } + /** + * Returns unique column names excluding duplicates. + * Columns to be removed: + * - if column definition already present in SELECT part with same alias + * - if column definition without alias already present in SELECT part without alias too + * @param array $columns the columns to be merged to the select. + * @since 2.0.14 + */ + protected function getUniqueColumns($columns) + { + $columns = array_unique($columns); + $unaliasedColumns = $this->getUnaliasedColumnsFromSelect(); + + foreach ($columns as $columnAlias => $columnDefinition) { + if ($columnDefinition instanceof Query) { + continue; + } + + if ( + (is_string($columnAlias) && isset($this->select[$columnAlias]) && $this->select[$columnAlias] === $columnDefinition) + || (is_integer($columnAlias) && in_array($columnDefinition, $unaliasedColumns)) + ) { + unset($columns[$columnAlias]); + } + } + return $columns; + } + + /** + * @return array List of columns without aliases from SELECT statement. + * @since 2.0.14 + */ + protected function getUnaliasedColumnsFromSelect() + { + $result = []; + if (is_array($this->select)) { + foreach ($this->select as $name => $value) { + if (is_integer($name)) { + $result[] = $value; + } + } + } + return array_unique($result); + } + /** * Sets the value indicating whether to SELECT DISTINCT or not. * @param bool $value whether to SELECT DISTINCT or not. @@ -1180,4 +1226,13 @@ PATTERN; 'params' => $from->params, ]); } + + /** + * Returns the SQL representation of Query + * @return string + */ + public function __toString() + { + return serialize($this); + } } diff --git a/tests/framework/db/QueryTest.php b/tests/framework/db/QueryTest.php index 6448a35942..5ddda5d5b3 100644 --- a/tests/framework/db/QueryTest.php +++ b/tests/framework/db/QueryTest.php @@ -37,6 +37,30 @@ abstract class QueryTest extends DatabaseTestCase $query->select('id, name'); $query->addSelect('email'); $this->assertEquals(['id', 'name', 'email'], $query->select); + + $query = new Query(); + $query->select('name, lastname'); + $query->addSelect('name'); + $this->assertEquals(['name', 'lastname'], $query->select); + + $query = new Query(); + $query->addSelect(['*', 'abc']); + $query->addSelect(['*', 'bca']); + $this->assertEquals(['*', 'abc', 'bca'], $query->select); + + $query = new Query(); + $query->addSelect(['field1 as a', 'field 1 as b']); + $this->assertEquals(['field1 as a', 'field 1 as b'], $query->select); + + $query = new Query(); + $query->select(['name' => 'firstname', 'lastname']); + $query->addSelect(['firstname', 'surname' => 'lastname']); + $query->addSelect(['firstname', 'lastname']); + $this->assertEquals(['name' => 'firstname', 'lastname', 'firstname', 'surname' => 'lastname'], $query->select); + + $query = new Query(); + $query->select('name, name, name as X, name as X'); + $this->assertEquals(['name', 'name as X'], array_values($query->select)); } public function testFrom()