Skip to content

Commit

Permalink
Merge branch 'master' into refactor-batchInsert
Browse files Browse the repository at this point in the history
  • Loading branch information
Tigrov committed Apr 14, 2024
2 parents 499972a + 3f977a5 commit 705b317
Show file tree
Hide file tree
Showing 30 changed files with 1,147 additions and 198 deletions.
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# Yii Database Change Log

## 1.3.1 under development
## 2.0.0 under development

- no changes in this release.
- 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
47 changes: 35 additions & 12 deletions UPGRADE.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,37 @@
# Upgrading Instructions for Yii Database

This file contains the upgrade notes for the Yii Database.
These notes highlight changes that could break your application when you upgrade it from one version to another.
Even though we try to ensure backwards compatibility (BC) as much as possible, sometimes
it isn't possible or very complicated to avoid it and still create a good solution to
a problem. While upgrade to Yii 3.0 might require substantial changes to both your application and extensions,
the changes are bearable and require "refactoring", not "rewrite".
All the "Yes, it is" cool stuff, and Yii soul is still in place.

Changes summary:

* `Yiisoft\Db\Connection::$charset` has been removed. All supported PDO classes allow you to specify the connection
charset in the DSN.
The following upgrading instructions are cumulative. That is, if you want to upgrade from version A to version C and
there is version B between A and C, you need to following the instructions for both A and 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:

- `Yiisoft\Db\Command\CommandInterface`;
- `Yiisoft\Db\QueryBuilder\DDLQueryBuilderInterface`;

… or inherit from the following classes:

- `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`.
2 changes: 1 addition & 1 deletion src/Command/AbstractCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public function addCheck(string $table, string $name, string $expression): stati
return $this->setSql($sql)->requireTableSchemaRefresh($table);
}

public function addColumn(string $table, string $column, string $type): static
public function addColumn(string $table, string $column, ColumnInterface|string $type): static
{
$sql = $this->getQueryBuilder()->addColumn($table, $column, $type);
return $this->setSql($sql)->requireTableSchemaRefresh($table);
Expand Down
9 changes: 6 additions & 3 deletions src/Command/CommandInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ public function addCheck(string $table, string $name, string $expression): stati
*
* @param string $table The name of the table to add new column to.
* @param string $column The name of the new column.
* @param string $type The column type. {@see QueryBuilder::getColumnType()} will be called to convert the given
* column type to the database one.
* @param ColumnInterface|string $type The column type. {@see QueryBuilder::getColumnType()} will be called
* to convert the given column type to the database one.
* For example, `string` will be converted to `varchar(255)`, and `string not null` becomes `varchar(255) not null`.
*
* Note: The method will quote the `table` and `column` parameters before using them in the generated SQL.
*/
public function addColumn(string $table, string $column, string $type): static;
public function addColumn(string $table, string $column, ColumnInterface|string $type): static;

/**
* Builds an SQL command for adding a comment to a column.
Expand Down Expand Up @@ -309,13 +309,16 @@ public function createIndex(
*
* @param string $table The name of the table to create.
* @param array $columns The columns (name => definition) in the new table.
* The definition can be `string` or {@see ColumnInterface} instance.
* @param string|null $options More SQL fragments to append to the generated SQL.
*
* @throws Exception
* @throws InvalidConfigException
* @throws NotSupportedException
*
* Note: The method will quote the `table` and `columns` parameter before using it in the generated SQL.
*
* @psalm-param array<string, ColumnInterface>|string[] $columns
*/
public function createTable(string $table, array $columns, string $options = null): static;

Expand Down
2 changes: 1 addition & 1 deletion src/Debug/CommandInterfaceProxy.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public function addCheck(string $table, string $name, string $expression): stati
/**
* @psalm-suppress MixedArgument
*/
public function addColumn(string $table, string $column, string $type): static
public function addColumn(string $table, string $column, ColumnInterface|string $type): static
{
return new self($this->decorated->{__FUNCTION__}(...func_get_args()), $this->collector);
}
Expand Down
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.

Loading

0 comments on commit 705b317

Please sign in to comment.