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

Process expressions inside param values when build a query #806

Merged
merged 34 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e1f142f
Build expressions if they inside `Expression::$params`
Tigrov Feb 5, 2024
c60f3a1
Improve
Tigrov Feb 5, 2024
f15c515
Fix errors
Tigrov Feb 5, 2024
17eb937
Fix errors
Tigrov Feb 6, 2024
b325bab
Update tests
Tigrov Feb 6, 2024
54dedfb
Apply fixes from StyleCI
StyleCIBot Feb 6, 2024
02708bb
Add tests
Tigrov Feb 11, 2024
5984d34
Resolve BC, make `$queryBuilder` optional in the constructor.
Tigrov Feb 11, 2024
7803afb
Apply fixes from StyleCI
StyleCIBot Feb 11, 2024
0845c13
Merge branch 'master' into fix-update-with-expressions
Tigrov Feb 11, 2024
8f76d49
Fix psalm issue
Tigrov Feb 11, 2024
b922159
Improve
Tigrov Feb 13, 2024
a5bff4a
Add test for indexed params
Tigrov Feb 13, 2024
be292a7
Merge branch 'master' into fix-update-with-expressions
vjik Mar 13, 2024
766fc54
Merge branch 'master' into fix-update-with-expressions
Tigrov Mar 25, 2024
e9660dc
Fix using regex when string value containing a placeholder name
Tigrov Mar 28, 2024
5daa211
Add SqlParser to fix case when string value containing a placeholder …
Tigrov Mar 30, 2024
27ad79f
Apply fixes from StyleCI
StyleCIBot Mar 30, 2024
79395aa
Fix psalm issue
Tigrov Mar 30, 2024
6f2cbb5
Cover tests
Tigrov Mar 30, 2024
04696b9
Cover tests
Tigrov Mar 30, 2024
026a51d
Cover tests
Tigrov Mar 30, 2024
011e68c
Optimization
Tigrov Apr 1, 2024
6d1de52
Make `SqlParser` abstract. Add test for `Param` and fix
Tigrov Apr 1, 2024
f8f226a
Fix array merge for php8.0
Tigrov Apr 1, 2024
42c263e
Update comments and remove `null` for `$queryBuilder`
Tigrov Apr 2, 2024
4c22f58
Add lines to CHANGELOG.md and UPGRADE.md [skip ci]
Tigrov Apr 6, 2024
76bac2c
Update CHANGELOG.md
Tigrov Apr 6, 2024
2731b3a
Apply Rector changes (CI)
Tigrov Apr 6, 2024
3c3f944
Apply fixes from StyleCI
StyleCIBot Apr 6, 2024
6dd4791
Apply suggestions from code review [skip ci]
Tigrov Apr 9, 2024
03fe6dc
Merge branch 'master' into fix-update-with-expressions
Tigrov Apr 9, 2024
5885a6f
Improve UPGRADE.md
Tigrov Apr 9, 2024
fe50046
Add test with UTF-8 multibyte symbols
Tigrov Apr 9, 2024
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
99 changes: 97 additions & 2 deletions src/Expression/ExpressionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@

namespace Yiisoft\Db\Expression;

use Yiisoft\Db\QueryBuilder\QueryBuilderInterface;

use function array_intersect_key;
use function array_merge;
use function preg_quote;
use function preg_replace;

/**
* It's used to build expressions for use in database queries.
Expand All @@ -17,9 +22,99 @@
*/
class ExpressionBuilder implements ExpressionBuilderInterface
{
public function __construct(private QueryBuilderInterface|null $queryBuilder = null)
vjik marked this conversation as resolved.
Show resolved Hide resolved
{
}

public function build(Expression $expression, array &$params = []): string
{
$params = array_merge($params, $expression->getParams());
return $expression->__toString();
$sql = $expression->__toString();
$expressionParams = $expression->getParams();

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

if ($this->queryBuilder === null || isset($expressionParams[0])) {
$params = array_merge($params, $expressionParams);
vjik marked this conversation as resolved.
Show resolved Hide resolved
return $sql;
}

$sql = $this->appendParams($sql, $expressionParams, $params);

return $this->replaceParamExpressions($sql, $expressionParams, $params);
}

private function appendParams(string $sql, array &$expressionParams, array &$params): string
{
$nonUniqueParams = array_intersect_key($expressionParams, $params);
$params += $expressionParams;

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

$patterns = [];
$replacements = [];

/** @var string $name */
vjik marked this conversation as resolved.
Show resolved Hide resolved
foreach ($nonUniqueParams as $name => $value) {
$patterns[] = $this->getPattern($name);
$uniqueName = $this->getUniqueName($name, $params);

$replacements[] = $uniqueName[0] !== ':' ? ":$uniqueName" : $uniqueName;

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

return preg_replace($patterns, $replacements, $sql, 1);
vjik marked this conversation as resolved.
Show resolved Hide resolved
}

private function replaceParamExpressions(string $sql, array $expressionParams, array &$params): string
{
$patterns = [];
$replacements = [];

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

$patterns[] = $this->getPattern($name);
/** @psalm-suppress PossiblyNullReference */
$replacements[] = $this->queryBuilder->buildExpression($value, $params);

unset($params[$name]);

Check failure on line 90 in src/Expression/ExpressionBuilder.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.2-ubuntu-latest

InvalidArrayAccess

src/Expression/ExpressionBuilder.php:90:19: InvalidArrayAccess: Cannot access array value on non-array variable $params of type type-alias(Yiisoft\Db\Connection\ConnectionInterface::ParamsType) (see https://psalm.dev/005)

Check failure on line 90 in src/Expression/ExpressionBuilder.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.3-ubuntu-latest

InvalidArrayAccess

src/Expression/ExpressionBuilder.php:90:19: InvalidArrayAccess: Cannot access array value on non-array variable $params of type type-alias(Yiisoft\Db\Connection\ConnectionInterface::ParamsType) (see https://psalm.dev/005)

Check failure on line 90 in src/Expression/ExpressionBuilder.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.1-ubuntu-latest

InvalidArrayAccess

src/Expression/ExpressionBuilder.php:90:19: InvalidArrayAccess: Cannot access array value on non-array variable $params of type type-alias(Yiisoft\Db\Connection\ConnectionInterface::ParamsType) (see https://psalm.dev/005)

Check failure on line 90 in src/Expression/ExpressionBuilder.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.3-ubuntu-latest

InvalidArrayAccess

src/Expression/ExpressionBuilder.php:90:19: InvalidArrayAccess: Cannot access array value on non-array variable $params of type type-alias(Yiisoft\Db\Connection\ConnectionInterface::ParamsType) (see https://psalm.dev/005)

Check failure on line 90 in src/Expression/ExpressionBuilder.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.1-ubuntu-latest

InvalidArrayAccess

src/Expression/ExpressionBuilder.php:90:19: InvalidArrayAccess: Cannot access array value on non-array variable $params of type type-alias(Yiisoft\Db\Connection\ConnectionInterface::ParamsType) (see https://psalm.dev/005)

Check failure on line 90 in src/Expression/ExpressionBuilder.php

View workflow job for this annotation

GitHub Actions / psalm / PHP 8.2-ubuntu-latest

InvalidArrayAccess

src/Expression/ExpressionBuilder.php:90:19: InvalidArrayAccess: Cannot access array value on non-array variable $params of type type-alias(Yiisoft\Db\Connection\ConnectionInterface::ParamsType) (see https://psalm.dev/005)
}

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

return preg_replace($patterns, $replacements, $sql, 1);
}

/** @psalm-return non-empty-string */
private function getPattern(string $name): string
{
if ($name[0] !== ':') {
$name = ":$name";
}

return '/' . preg_quote($name, '/') . '\b/';
}

private function getUniqueName(string $name, array $params): string
{
$uniqueName = $name . '_0';

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

return $uniqueName;
}
}
47 changes: 13 additions & 34 deletions src/QueryBuilder/AbstractDQLQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,25 +105,7 @@ public function build(QueryInterface $query, array $params = []): array
$this->buildHaving($query->getHaving(), $params),
];
$sql = implode($this->separator, array_filter($clauses));
$sql = $this->buildOrderByAndLimit($sql, $query->getOrderBy(), $query->getLimit(), $query->getOffset());

if (!empty($query->getOrderBy())) {
/** @psalm-var array<string, ExpressionInterface|string> */
foreach ($query->getOrderBy() as $expression) {
if ($expression instanceof ExpressionInterface) {
$this->buildExpression($expression, $params);
}
}
}

if (!empty($query->getGroupBy())) {
/** @psalm-var array<string, ExpressionInterface|string> */
foreach ($query->getGroupBy() as $expression) {
if ($expression instanceof ExpressionInterface) {
$this->buildExpression($expression, $params);
}
}
}
$sql = $this->buildOrderByAndLimit($sql, $query->getOrderBy(), $query->getLimit(), $query->getOffset(), $params);

$union = $this->buildUnion($query->getUnions(), $params);

Expand Down Expand Up @@ -165,19 +147,22 @@ public function buildColumns(array|string $columns): string

public function buildCondition(array|string|ExpressionInterface|null $condition, array &$params = []): string
{
if (is_array($condition)) {
if (empty($condition)) {
return '';
if (empty($condition)) {
if ($condition === '0') {
return '0';
}

$condition = $this->createConditionFromArray($condition);
return '';
}

if ($condition instanceof ExpressionInterface) {
return $this->buildExpression($condition, $params);
if (is_array($condition)) {
$condition = $this->createConditionFromArray($condition);
} elseif (is_string($condition)) {
$condition = new Expression($condition, $params);
$params = [];
}

return $condition ?? '';
return $this->buildExpression($condition, $params);
}

public function buildExpression(ExpressionInterface $expression, array &$params = []): string
Expand Down Expand Up @@ -208,10 +193,7 @@ public function buildGroupBy(array $columns, array &$params = []): string
/** @psalm-var array<string, ExpressionInterface|string> $columns */
foreach ($columns as $i => $column) {
if ($column instanceof ExpressionInterface) {
$columns[$i] = $this->buildExpression($column);
if ($column instanceof Expression || $column instanceof QueryInterface) {
$params = array_merge($params, $column->getParams());
}
$columns[$i] = $this->buildExpression($column, $params);
} elseif (!str_contains($column, '(')) {
$columns[$i] = $this->quoter->quoteColumnName($column);
}
Expand Down Expand Up @@ -299,10 +281,7 @@ public function buildOrderBy(array $columns, array &$params = []): string
/** @psalm-var array<string, ExpressionInterface|int|string> $columns */
foreach ($columns as $name => $direction) {
if ($direction instanceof ExpressionInterface) {
$orders[] = $this->buildExpression($direction);
if ($direction instanceof Expression || $direction instanceof QueryInterface) {
$params = array_merge($params, $direction->getParams());
}
$orders[] = $this->buildExpression($direction, $params);
} else {
$orders[] = $this->quoter->quoteColumnName($name) . ($direction === SORT_DESC ? ' DESC' : '');
}
Expand Down
16 changes: 9 additions & 7 deletions tests/AbstractQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2194,16 +2194,18 @@ public function testUpdate(
string $table,
array $columns,
array|string $condition,
string $expectedSQL,
array $params,
string $expectedSql,
array $expectedParams
): void {
$db = $this->getConnection();

$qb = $db->getQueryBuilder();
$actualParams = [];

$this->assertSame($expectedSQL, $qb->update($table, $columns, $condition, $actualParams));
$this->assertSame($expectedParams, $actualParams);
$sql = $qb->update($table, $columns, $condition, $params);
$sql = $qb->quoter()->quoteSql($sql);

$this->assertSame($expectedSql, $sql);
$this->assertEquals($expectedParams, $params);
}

/**
Expand Down Expand Up @@ -2274,7 +2276,7 @@ public function testOverrideParameters1(): void
{
$db = $this->getConnection();

$params = [':id' => 1, ':pv2' => new Expression('(select type from {{%animal}}) where id=1')];
$params = [':id' => 1, ':pv2' => 'test'];
$expression = new Expression('id = :id AND type = :pv2', $params);

$query = new Query($db);
Expand All @@ -2289,7 +2291,7 @@ public function testOverrideParameters1(): void
$this->assertEquals([':id', ':pv2', ':pv2_0',], array_keys($command->getParams()));
$this->assertEquals(
DbHelper::replaceQuotes(
'SELECT * FROM [[animal]] WHERE (id = 1 AND type = (select type from {{%animal}}) where id=1) AND ([[type]]=\'test1\')',
'SELECT * FROM [[animal]] WHERE (id = 1 AND type = \'test\') AND ([[type]]=\'test1\')',
$db->getDriverName()
),
$command->getRawSql()
Expand Down
19 changes: 15 additions & 4 deletions tests/Common/CommonCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1888,14 +1888,25 @@ public function testUpdate(
array $columns,
array|string $conditions,
array $params,
string $expected
array $expectedValues,
int $expectedCount,
): void {
$db = $this->getConnection();
$db = $this->getConnection(true);

$command = $db->createCommand();
$sql = $command->update($table, $columns, $conditions, $params)->getSql();
$count = $command->update($table, $columns, $conditions, $params)->execute();

$this->assertSame($expectedCount, $count);

$this->assertSame($expected, $sql);
$values = (new Query($db))
->from($table)
->where($conditions, $params)
->limit(1)
->one();

foreach ($expectedValues as $name => $expectedValue) {
$this->assertEquals($expectedValue, $values[$name]);
}

$db->close();
}
Expand Down
11 changes: 7 additions & 4 deletions tests/Db/QueryBuilder/QueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -253,17 +253,20 @@ public function testUpdate(
string $table,
array $columns,
array|string $condition,
string $expectedSQL,
array $params,
string $expectedSql,
array $expectedParams
): void {
$db = $this->getConnection();

$schemaMock = $this->createMock(Schema::class);
$qb = new QueryBuilder($db->getQuoter(), $schemaMock);
$actualParams = [];

$this->assertSame($expectedSQL, $qb->update($table, $columns, $condition, $actualParams));
$this->assertSame($expectedParams, $actualParams);
$sql = $qb->update($table, $columns, $condition, $params);
$sql = $qb->quoter()->quoteSql($sql);

$this->assertSame($expectedSql, $sql);
$this->assertEquals($expectedParams, $params);
}

/**
Expand Down
Loading
Loading