From 3f977a55a05a0f47a2008f59a91692aa490e136b Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Sat, 13 Apr 2024 15:02:47 +0700 Subject: [PATCH] Allow scalar values for `Query::select()` (#816) * Allow scalar values for `Query::select()` * Apply fixes from StyleCI * Apply Rector changes (CI) * Apply fixes from StyleCI * Update CHANGELOG.md and UPGRADE.md [skip ci] * Update CHANGELOG.md and UPGRADE.md [skip ci] * Update UPGRADE.md [skip ci] * Update UPGRADE.md [skip ci] * fix UPGRADE.md * improve `UPGRADE.md` * Improve psalm types for scalar values in select (#818) * Improve psalm types for scalar values in select * Remove unnecessary psalm annotations --------- Co-authored-by: Tigrov * Improve phpdoc [skip ci] * Update tests/Provider/QueryBuilderProvider.php Co-authored-by: Sergei Predvoditelev * Fix test * Add type string to test --------- Co-authored-by: StyleCI Bot Co-authored-by: Tigrov Co-authored-by: Sergei Predvoditelev --- CHANGELOG.md | 1 + UPGRADE.md | 8 ++++++ src/Query/Query.php | 27 +++++++++++-------- src/Query/QueryInterface.php | 2 ++ src/Query/QueryPartsInterface.php | 20 ++++++++++---- src/QueryBuilder/AbstractDQLQueryBuilder.php | 13 ++++++++- src/QueryBuilder/DQLQueryBuilderInterface.php | 3 +++ tests/AbstractQueryBuilderTest.php | 18 +++++++++++++ tests/AbstractQueryTest.php | 9 ++++++- tests/Provider/QueryBuilderProvider.php | 16 +++++++++++ tests/Provider/QueryProvider.php | 4 +++ 11 files changed, 103 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d34d06330..5eab3b8c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 2.0.0 under development +- Enh #816: Allow scalar values for `$columns` parameter of `Query::select()` and `Query::addSelect()` methods (@Tigrov) - Enh #806: Non-unique placeholder names inside `Expression::$params` will be replaced with unique names (@Tigrov) - Enh #806: Build `Expression` instances inside `Expression::$params` when build a query using `QueryBuilder` (@Tigrov) - Enh #766: Allow `ColumnInterface` as column type. (@Tigrov) diff --git a/UPGRADE.md b/UPGRADE.md index dde268f37..6b040126c 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -19,6 +19,14 @@ in `addColumn()` method of your classes that implement the following interfaces: - `Yiisoft\Db\QueryBuilder\AbstractDDLQueryBuilder`; - `Yiisoft\Db\QueryBuilder\AbstractQueryBuilder`. +### Scalar values for columns in `Query` + +Change `$columns` parameter type from `array|string|ExpressionInterface` to `array|bool|float|int|string|ExpressionInterface` +in methods `select()` and `addSelect()` of your classes that implement `Yiisoft\Db\Query\QueryPartsInterface`. + +Add support any scalar values for `$columns` parameter of these methods in your classes that implement +`Yiisoft\Db\Query\QueryPartsInterface` or inherit `Yiisoft\Db\Query\Query`. + ### Build `Expression` instances inside `Expression::$params` `ExpressionBuilder` is replaced by an abstract class `AbstractExpressionBuilder` with an instance of the diff --git a/src/Query/Query.php b/src/Query/Query.php index 8321d7a96..d76b16b34 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -21,6 +21,7 @@ use function array_shift; use function array_unshift; use function count; +use function gettype; use function is_array; use function is_int; use function is_numeric; @@ -66,9 +67,12 @@ * ``` * * Query internally uses the {@see \Yiisoft\Db\QueryBuilder\AbstractQueryBuilder} class to generate the SQL statement. + * + * @psalm-import-type SelectValue from QueryPartsInterface */ class Query implements QueryInterface { + /** @psalm-var SelectValue $select */ protected array $select = []; protected string|null $selectOption = null; protected bool|null $distinct = null; @@ -178,7 +182,7 @@ public function andHaving(array|string|ExpressionInterface $condition, array $pa return $this; } - public function addSelect(array|string|ExpressionInterface $columns): static + public function addSelect(array|bool|float|int|string|ExpressionInterface $columns): static { if ($this->select === []) { return $this->select($columns); @@ -612,7 +616,7 @@ public function scalar(): bool|int|null|string|float }; } - public function select(array|string|ExpressionInterface $columns, string $option = null): static + public function select(array|bool|float|int|string|ExpressionInterface $columns, string $option = null): static { $this->select = $this->normalizeSelect($columns); $this->selectOption = $option; @@ -854,18 +858,20 @@ private function normalizeOrderBy(array|string|ExpressionInterface $columns): ar /** * Normalizes the `SELECT` columns passed to {@see select()} or {@see addSelect()}. + * + * @psalm-param SelectValue|scalar|ExpressionInterface $columns + * @psalm-return SelectValue */ - private function normalizeSelect(array|ExpressionInterface|string $columns): array + private function normalizeSelect(array|bool|float|int|string|ExpressionInterface $columns): array { - if ($columns instanceof ExpressionInterface) { - $columns = [$columns]; - } elseif (!is_array($columns)) { - $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); - } + $columns = match (gettype($columns)) { + 'array' => $columns, + 'string' => preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY), + default => [$columns], + }; $select = []; - /** @psalm-var array $columns */ foreach ($columns as $columnAlias => $columnDefinition) { if (is_string($columnAlias)) { // Already in the normalized format, good for them. @@ -890,8 +896,7 @@ private function normalizeSelect(array|ExpressionInterface|string $columns): arr } } - // Either a string calling a function, DB expression, or sub-query - /** @psalm-var string */ + // Either a string calling a function, instance of ExpressionInterface or a scalar value. $select[] = $columnDefinition; } diff --git a/src/Query/QueryInterface.php b/src/Query/QueryInterface.php index 246d007bd..88b3afb8d 100644 --- a/src/Query/QueryInterface.php +++ b/src/Query/QueryInterface.php @@ -28,6 +28,7 @@ * Sorting is supported via {@see orderBy()} and items can be limited to match some conditions using {@see where()}. * * @psalm-import-type ParamsType from ConnectionInterface + * @psalm-import-type SelectValue from QueryPartsInterface */ interface QueryInterface extends ExpressionInterface, QueryPartsInterface, QueryFunctionsInterface { @@ -207,6 +208,7 @@ public function getParams(): array; /** * @return array The "select" value. + * @psalm-return SelectValue */ public function getSelect(): array; diff --git a/src/Query/QueryPartsInterface.php b/src/Query/QueryPartsInterface.php index 9d2b4dec2..9ab460430 100644 --- a/src/Query/QueryPartsInterface.php +++ b/src/Query/QueryPartsInterface.php @@ -15,6 +15,7 @@ * * {@see Query} uses these methods to build and manipulate SQL statements. * + * @psalm-type SelectValue = array * @psalm-import-type ParamsType from ConnectionInterface */ interface QueryPartsInterface @@ -64,11 +65,15 @@ public function addOrderBy(array|string|ExpressionInterface $columns): static; * $query->addSelect(["*", "CONCAT(first_name, ' ', last_name) AS full_name"])->one(); * ``` * - * @param array|ExpressionInterface|string $columns The columns to add to the select. + * @param array|ExpressionInterface|scalar $columns The columns to add to the select. * - * {@see select()} for more details about the format of this parameter. + * @see select() for more details about the format of this parameter. + * + * @since 2.0.0 `$columns` can be a scalar value or an array of scalar values. + * + * @psalm-param SelectValue|scalar|ExpressionInterface $columns */ - public function addSelect(array|string|ExpressionInterface $columns): static; + public function addSelect(array|bool|float|int|string|ExpressionInterface $columns): static; /** * Adds a filtering condition for a specific column and allow the user to choose a filter operator. @@ -514,7 +519,7 @@ public function rightJoin(array|string $table, array|string $on = '', array $par /** * Sets the `SELECT` part of the query. * - * @param array|ExpressionInterface|string $columns The columns to be selected. + * @param array|ExpressionInterface|scalar $columns The columns to be selected. * Columns can be specified in either a string (for example `id, name`) or an array (such as `['id', 'name']`). * Columns can be prefixed with table names (such as `user.id`) and/or contain column aliases * (for example `user.id AS user_id`). @@ -527,8 +532,13 @@ public function rightJoin(array|string $table, array|string $on = '', array $par * doesn't need alias, don't use a string key). * @param string|null $option More option that should be appended to the 'SELECT' keyword. For example, in MySQL, * the option 'SQL_CALC_FOUND_ROWS' can be used. + * + * @since 2.0.0 `$columns` can be a scalar value or an array of scalar values. + * For example, `$query->select(1)` will be converted to `SELECT 1`. + * + * @psalm-param SelectValue|scalar|ExpressionInterface $columns */ - public function select(array|string|ExpressionInterface $columns, string $option = null): static; + public function select(array|bool|float|int|string|ExpressionInterface $columns, string $option = null): static; /** * It allows you to specify more options for the `SELECT` clause of an SQL statement. diff --git a/src/QueryBuilder/AbstractDQLQueryBuilder.php b/src/QueryBuilder/AbstractDQLQueryBuilder.php index fd3951540..9119ffa7d 100644 --- a/src/QueryBuilder/AbstractDQLQueryBuilder.php +++ b/src/QueryBuilder/AbstractDQLQueryBuilder.php @@ -13,6 +13,7 @@ use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Expression\ExpressionBuilderInterface; use Yiisoft\Db\Expression\ExpressionInterface; +use Yiisoft\Db\Helper\DbStringHelper; use Yiisoft\Db\QueryBuilder\Condition\HashCondition; use Yiisoft\Db\QueryBuilder\Condition\Interface\ConditionInterface; use Yiisoft\Db\QueryBuilder\Condition\SimpleCondition; @@ -25,6 +26,7 @@ use function array_merge; use function array_shift; use function ctype_digit; +use function gettype; use function implode; use function is_array; use function is_int; @@ -324,7 +326,6 @@ public function buildSelect( return $select . ' *'; } - /** @psalm-var array $columns */ foreach ($columns as $i => $column) { if ($column instanceof ExpressionInterface) { if (is_int($i)) { @@ -333,6 +334,16 @@ public function buildSelect( $columns[$i] = $this->buildExpression($column, $params) . ' AS ' . $this->quoter->quoteColumnName($i); } + } elseif (!is_string($column)) { + $columns[$i] = match (gettype($column)) { + 'double' => DbStringHelper::normalizeFloat($column), + 'boolean' => $column ? 'TRUE' : 'FALSE', + default => (string) $column, + }; + + if (is_string($i)) { + $columns[$i] .= ' AS ' . $this->quoter->quoteColumnName($i); + } } elseif (is_string($i) && $i !== $column) { if (!str_contains($column, '(')) { $column = $this->quoter->quoteColumnName($column); diff --git a/src/QueryBuilder/DQLQueryBuilderInterface.php b/src/QueryBuilder/DQLQueryBuilderInterface.php index dd2076dbb..6a53585cb 100644 --- a/src/QueryBuilder/DQLQueryBuilderInterface.php +++ b/src/QueryBuilder/DQLQueryBuilderInterface.php @@ -11,6 +11,7 @@ use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Expression\ExpressionBuilderInterface; use Yiisoft\Db\Expression\ExpressionInterface; +use Yiisoft\Db\Query\QueryPartsInterface; use Yiisoft\Db\QueryBuilder\Condition\Interface\ConditionInterface; use Yiisoft\Db\Query\QueryInterface; @@ -20,6 +21,7 @@ * @link https://en.wikipedia.org/wiki/Data_query_language * * @psalm-import-type ParamsType from ConnectionInterface + * @psalm-import-type SelectValue from QueryPartsInterface */ interface DQLQueryBuilderInterface { @@ -224,6 +226,7 @@ public function buildOrderByAndLimit( * * @return string The `SELECT` clause built from {@see \Yiisoft\Db\Query\Query::select()}. * + * @psalm-param SelectValue $columns * @psalm-param ParamsType $params */ public function buildSelect( diff --git a/tests/AbstractQueryBuilderTest.php b/tests/AbstractQueryBuilderTest.php index 4ab4f287f..35cf22c43 100644 --- a/tests/AbstractQueryBuilderTest.php +++ b/tests/AbstractQueryBuilderTest.php @@ -2091,6 +2091,24 @@ public function testSelectSubquery(): void $this->assertEmpty($params); } + /** @dataProvider \Yiisoft\Db\Tests\Provider\QueryBuilderProvider::selectScalar */ + public function testSelectScalar(array|bool|float|int|string $columns, string $expected): void + { + $db = $this->getConnection(); + $qb = $db->getQueryBuilder(); + + $query = (new Query($db))->select($columns); + + [$sql, $params] = $qb->build($query); + + if ($db->getDriverName() === 'oci') { + $expected .= ' FROM DUAL'; + } + + $this->assertSame($expected, $sql); + $this->assertEmpty($params); + } + public function testSetConditionClasses(): void { $db = $this->getConnection(); diff --git a/tests/AbstractQueryTest.php b/tests/AbstractQueryTest.php index 754cab5a3..cae70b93f 100644 --- a/tests/AbstractQueryTest.php +++ b/tests/AbstractQueryTest.php @@ -661,6 +661,13 @@ public function testSelect(): void ['DISTINCT ON(tour_dates.date_from) tour_dates.date_from', 'tour_dates.id' => 'tour_dates.id'], $query->getSelect() ); + + $query = new Query($db); + $query->select(1); + $query->addSelect(true); + $query->addSelect(['float' => 12.34]); + + $this->assertSame([1, true, 'float' => 12.34], $query->getSelect()); } public function testSetJoin(): void @@ -779,7 +786,7 @@ public function testNormalizeOrderBy(array|string|Expression $columns, array|str /** * @dataProvider \Yiisoft\Db\Tests\Provider\QueryProvider::normalizeSelect */ - public function testNormalizeSelect(array|string|Expression $columns, array|string $expected): void + public function testNormalizeSelect(array|bool|float|int|string|ExpressionInterface $columns, array|string $expected): void { $query = (new Query($this->getConnection())); $this->assertEquals([], $query->getSelect()); diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index 77122b9c9..f8f0b5356 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -1112,6 +1112,22 @@ public static function selectExist(): array ]; } + public static function selectScalar(): array + { + return [ + [1, 'SELECT 1'], + ['custom_string', DbHelper::replaceQuotes('SELECT [[custom_string]]', static::$driverName)], + [true, 'SELECT TRUE'], + [false, 'SELECT FALSE'], + [12.34, 'SELECT 12.34'], + [[1, true, 12.34], 'SELECT 1, TRUE, 12.34'], + [ + ['a' => 1, 'b' => true, 12.34], + DbHelper::replaceQuotes('SELECT 1 AS [[a]], TRUE AS [[b]], 12.34', static::$driverName), + ], + ]; + } + public static function update(): array { return [ diff --git a/tests/Provider/QueryProvider.php b/tests/Provider/QueryProvider.php index 6833f7f4c..274cf2147 100644 --- a/tests/Provider/QueryProvider.php +++ b/tests/Provider/QueryProvider.php @@ -73,6 +73,10 @@ public static function normalizeSelect(): array ['email' => 'email', 'address' => 'address', 'status' => new Expression('1')], ], [new Expression('1 as Ab'), [new Expression('1 as Ab')]], + [1, [1]], + [true, [true]], + [12.34, [12.34]], + [['a' => 1, 'b' => true, 12.34], ['a' => 1, 'b' => true, 12.34]], ]; } }