Skip to content

Commit

Permalink
Merge branch 'master' into refactor-Query-column
Browse files Browse the repository at this point in the history
  • Loading branch information
Tigrov committed Apr 15, 2024
2 parents 629458e + 3f977a5 commit 29a009d
Show file tree
Hide file tree
Showing 23 changed files with 1,069 additions and 161 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
## 2.0.0 under development

- Enh #815: Refactor `Query::column()` method (@Tigrov)
- 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)

## 1.3.0 March 21, 2024
Expand Down
19 changes: 19 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ there is version B between A and C, you need to following the instructions for b

## Upgrade from 1.x to 2.x

### `ColumnInterface` as column type

Add `ColumnInterface` support and change type of parameter `$type` from `string` to `ColumnInterface|string`
in `addColumn()` method of your classes that implement the following interfaces:

Expand All @@ -16,3 +18,20 @@ in `addColumn()` method of your classes that implement the following interfaces:
- `Yiisoft\Db\Command\AbstractCommand`;
- `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
`QueryBuilderInterface` parameter in the constructor. Each DBMS driver should implement its own expression builder.

`Expression::$params` can contain:
- non-unique placeholder names, they will be replaced with unique names.
- `Expression` instances, they will be built when building a query using `QueryBuilder`.
242 changes: 242 additions & 0 deletions src/Expression/AbstractExpressionBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Expression;

use Yiisoft\Db\Command\Param;
use Yiisoft\Db\Connection\ConnectionInterface;
use Yiisoft\Db\QueryBuilder\QueryBuilderInterface;
use Yiisoft\Db\Syntax\AbstractSqlParser;

use function array_merge;
use function count;
use function strlen;
use function substr;
use function substr_replace;

/**
* It's used to build expressions for use in database queries.
*
* It provides a {@see build()} method for creating various types of expressions, such as conditions, joins, and
* ordering clauses.
*
* These expressions can be used with the query builder to build complex and customizable database queries
* {@see Expression} class.
*
* @psalm-import-type ParamsType from ConnectionInterface
*/
abstract class AbstractExpressionBuilder implements ExpressionBuilderInterface
{
public function __construct(private QueryBuilderInterface $queryBuilder)
{
}

/**
* Builds an SQL expression from the given expression object.
*
* This method is called by the query builder to build SQL expressions from {@see ExpressionInterface} objects.
*
* @param Expression $expression The expression to build.
* @param array $params The parameters to be bound to the query.
*
* @psalm-param ParamsType $params
*
* @return string SQL expression.
*/
public function build(ExpressionInterface $expression, array &$params = []): string
{
$sql = $expression->__toString();
$expressionParams = $expression->getParams();

if (empty($expressionParams)) {
return $sql;
}

if (isset($expressionParams[0])) {
$params = array_merge($params, $expressionParams);
return $sql;
}

$nonUniqueReplacements = $this->appendParams($expressionParams, $params);
$expressionReplacements = $this->buildParamExpressions($expressionParams, $params);

$replacements = $this->mergeReplacements($nonUniqueReplacements, $expressionReplacements);

if (empty($replacements)) {
return $sql;
}

return $this->replacePlaceholders($sql, $replacements);
}

/**
* Appends parameters to the list of query parameters replacing non-unique parameters with unique ones.
*
* @param array $expressionParams Parameters to be appended.
* @param array $params Parameters to be bound to the query.
*
* @psalm-param ParamsType $expressionParams
* @psalm-param ParamsType $params
*
* @return string[] Replacements for non-unique parameters.
*/
private function appendParams(array &$expressionParams, array &$params): array
{
$nonUniqueParams = [];

/** @var non-empty-string $name */
foreach ($expressionParams as $name => $value) {
$paramName = $name[0] === ':' ? substr($name, 1) : $name;

if (!isset($params[$paramName]) && !isset($params[":$paramName"])) {
$params[$name] = $value;
continue;
}

$nonUniqueParams[$name] = $value;
}

$replacements = [];

/** @var non-empty-string $name */
foreach ($nonUniqueParams as $name => $value) {
$paramName = $name[0] === ':' ? substr($name, 1) : $name;
$uniqueName = $this->getUniqueName($paramName, $params);

$replacements[":$paramName"] = ":$uniqueName";

if ($name[0] === ':') {
$uniqueName = ":$uniqueName";
}

$params[$uniqueName] = $value;
$expressionParams[$uniqueName] = $value;
unset($expressionParams[$name]);
}

return $replacements;
}

/**
* Build expression values of parameters.
*
* @param array $expressionParams Parameters from the expression.
* @param array $params Parameters to be bound to the query.
*
* @psalm-param ParamsType $expressionParams
* @psalm-param ParamsType $params
*
* @return string[] Replacements for parameters.
*/
private function buildParamExpressions(array $expressionParams, array &$params): array
{
$replacements = [];

/** @var non-empty-string $name */
foreach ($expressionParams as $name => $value) {
if (!$value instanceof ExpressionInterface || $value instanceof Param) {
continue;
}

$placeholder = $name[0] !== ':' ? ":$name" : $name;
$replacements[$placeholder] = $this->queryBuilder->buildExpression($value, $params);

/** @psalm-var ParamsType $params */
unset($params[$name]);
}

return $replacements;
}

/**
* Merges replacements for non-unique parameters with replacements for expression parameters.
*
* @param string[] $replacements Replacements for non-unique parameters.
* @param string[] $expressionReplacements Replacements for expression parameters.
*
* @return string[] Merged replacements.
*/
private function mergeReplacements(array $replacements, array $expressionReplacements): array
{
if (empty($replacements)) {
return $expressionReplacements;
}

if (empty($expressionReplacements)) {
return $replacements;
}

/** @var non-empty-string $value */
foreach ($replacements as $name => $value) {
if (isset($expressionReplacements[$value])) {
$replacements[$name] = $expressionReplacements[$value];
unset($expressionReplacements[$value]);
}
}

return $replacements + $expressionReplacements;
}

/**
* Returns a unique name for the parameter without colon at the beginning.
*
* @param string $name Name of the parameter without colon at the beginning.
* @param array $params Parameters to be bound to the query.
*
* @psalm-param ParamsType $params
*
* @return string Unique name of the parameter with colon at the beginning.
*
* @psalm-return non-empty-string
*/
private function getUniqueName(string $name, array $params): string
{
$uniqueName = $name . '_0';

for ($i = 1; isset($params[$uniqueName]) || isset($params[":$uniqueName"]); ++$i) {
$uniqueName = $name . '_' . $i;
}

return $uniqueName;
}

/**
* Replaces placeholders with replacements in a SQL expression.
*
* @param string $sql SQL expression where the placeholder should be replaced.
* @param string[] $replacements Replacements for placeholders.
*
* @return string SQL expression with replaced placeholders.
*/
private function replacePlaceholders(string $sql, array $replacements): string
{
$parser = $this->createSqlParser($sql);
$offset = 0;

while (null !== $placeholder = $parser->getNextPlaceholder($position)) {
if (isset($replacements[$placeholder])) {
/** @var int $position */
$sql = substr_replace($sql, $replacements[$placeholder], $position + $offset, strlen($placeholder));

if (count($replacements) === 1) {
break;
}

$offset += strlen($replacements[$placeholder]) - strlen($placeholder);
unset($replacements[$placeholder]);
}
}

return $sql;
}

/**
* Creates an instance of {@see AbstractSqlParser} for the given SQL expression.
*
* @param string $sql SQL expression to be parsed.
*
* @return AbstractSqlParser SQL parser instance.
*/
abstract protected function createSqlParser(string $sql): AbstractSqlParser;
}
25 changes: 0 additions & 25 deletions src/Expression/ExpressionBuilder.php

This file was deleted.

27 changes: 16 additions & 11 deletions src/Query/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,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 @@ -68,9 +69,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 @@ -181,7 +185,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 @@ -613,7 +617,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 @@ -855,18 +859,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 @@ -891,8 +897,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
Loading

0 comments on commit 29a009d

Please sign in to comment.