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

Allow scalar values for Query::select() #816

Merged
merged 17 commits into from
Apr 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 16 additions & 11 deletions src/Query/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<array-key, ExpressionInterface|string> $columns */
foreach ($columns as $columnAlias => $columnDefinition) {
if (is_string($columnAlias)) {
// Already in the normalized format, good for them.
Expand All @@ -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;
}

Expand Down
2 changes: 2 additions & 0 deletions src/Query/QueryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -207,6 +208,7 @@ public function getParams(): array;

/**
* @return array The "select" value.
* @psalm-return SelectValue
*/
public function getSelect(): array;

Expand Down
20 changes: 15 additions & 5 deletions src/Query/QueryPartsInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*
* {@see Query} uses these methods to build and manipulate SQL statements.
*
* @psalm-type SelectValue = array<array-key, ExpressionInterface|scalar>
* @psalm-import-type ParamsType from ConnectionInterface
*/
interface QueryPartsInterface
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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`).
Expand All @@ -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.
Expand Down
13 changes: 12 additions & 1 deletion src/QueryBuilder/AbstractDQLQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -324,7 +326,6 @@ public function buildSelect(
return $select . ' *';
}

/** @psalm-var array<array-key, ExpressionInterface|string> $columns */
foreach ($columns as $i => $column) {
if ($column instanceof ExpressionInterface) {
if (is_int($i)) {
Expand All @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions src/QueryBuilder/DQLQueryBuilderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
{
Expand Down Expand Up @@ -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(
Expand Down
18 changes: 18 additions & 0 deletions tests/AbstractQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
9 changes: 8 additions & 1 deletion tests/AbstractQueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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());
Expand Down
16 changes: 16 additions & 0 deletions tests/Provider/QueryBuilderProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 [
Expand Down
4 changes: 4 additions & 0 deletions tests/Provider/QueryProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]],
];
}
}
Loading