Skip to content
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
- 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 #806, #964: Build `Expression` instances inside `Expression::$params` when build a query using `QueryBuilder` (@Tigrov)
- Enh #766: Allow `ColumnInterface` as column type. (@Tigrov)
- Bug #828: Fix `float` type when use `AbstractCommand::getRawSql()` method (@Tigrov)
- New #752: Implement `ColumnSchemaInterface` classes according to the data type of database table columns
Expand Down Expand Up @@ -84,6 +84,7 @@
- Enh #961: Added `setHaving()` as a forced method to overwrite `having()` (@lav45)
- Enh #822: Refactor data readers (@Tigrov)
- Enh #963: Make `Query::andHaving()` similar to `Query::andWhere()` (@Tigrov)
- New #964: Add `QueryBuilderInterface::replacePlaceholders()` method (@Tigrov)

## 1.3.0 March 21, 2024

Expand Down
1 change: 1 addition & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ Each table column has its own class in the `Yiisoft\Db\Schema\Column` namespace
- `QueryBuilderInterface::buildColumnDefinition()` - builds column definition for `CREATE TABLE` statement;
- `QueryBuilderInterface::prepareParam()` - converts a `ParamInterface` object to its SQL representation;
- `QueryBuilderInterface::prepareValue()` - converts a value to its SQL representation;
- `QueryBuilderInterface::replacePlaceholders()` - replaces placeholders in the SQL string with the corresponding values;
- `QueryBuilderInterface::getColumnFactory()` - returns the column factory object for concrete DBMS;
- `QueryBuilderInterface::getServerInfo()` - returns `ServerInfoInterface` instance which provides server information;
- `DMLQueryBuilderInterface::withTypecasting()` - enables or disables typecasting of values when inserting or updating
Expand Down
21 changes: 3 additions & 18 deletions src/Command/AbstractCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,13 @@
use Yiisoft\Db\QueryBuilder\QueryBuilderInterface;
use Yiisoft\Db\Schema\Column\ColumnInterface;

use function array_map;
use function explode;
use function get_resource_type;
use function is_array;
use function is_int;
use function is_resource;
use function is_scalar;
use function is_string;
use function preg_replace_callback;
use function stream_get_contents;

/**
Expand Down Expand Up @@ -348,25 +347,11 @@ public function getRawSql(): string
return $this->sql;
}

$params = [];
$queryBuilder = $this->getQueryBuilder();
$params = array_map($queryBuilder->prepareParam(...), $this->params);

foreach ($this->params as $name => $param) {
if (is_string($name) && $name[0] !== ':') {
$name = ':' . $name;
}

$params[$name] = $queryBuilder->prepareParam($param);
}

/** @var string[] $params */
if (!isset($params[0])) {
/** @var string */
return preg_replace_callback(
'#(:\w+)#',
static fn (array $matches): string => $params[$matches[1]] ?? $matches[1],
$this->sql
);
return $queryBuilder->replacePlaceholders($this->sql, $params);
}

// Support unnamed placeholders should be dropped
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,9 @@
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.
Expand All @@ -26,9 +22,9 @@
*
* @psalm-import-type ParamsType from ConnectionInterface
*/
abstract class AbstractExpressionBuilder implements ExpressionBuilderInterface
final class ExpressionBuilder implements ExpressionBuilderInterface
{
public function __construct(private QueryBuilderInterface $queryBuilder)
public function __construct(private readonly QueryBuilderInterface $queryBuilder)
{
}

Expand Down Expand Up @@ -67,7 +63,7 @@ public function build(ExpressionInterface $expression, array &$params = []): str
return $sql;
}

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

/**
Expand Down Expand Up @@ -199,43 +195,4 @@ private function getUniqueName(string $name, array $params): string

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;
}
8 changes: 3 additions & 5 deletions src/QueryBuilder/AbstractDDLQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Yiisoft\Db\Schema\QuoterInterface;
use Yiisoft\Db\Schema\SchemaInterface;

use function array_map;
use function implode;
use function is_string;
use function preg_split;
Expand Down Expand Up @@ -193,11 +194,8 @@ public function createView(string $viewName, QueryInterface|string $subQuery): s
if ($subQuery instanceof QueryInterface) {
[$rawQuery, $params] = $this->queryBuilder->build($subQuery);

foreach ($params as $key => $value) {
$params[$key] = $this->queryBuilder->prepareValue($value);
}

$subQuery = strtr($rawQuery, $params);
$params = array_map($this->queryBuilder->prepareValue(...), $params);
$subQuery = $this->queryBuilder->replacePlaceholders($rawQuery, $params);
}

return 'CREATE VIEW ' . $this->quoter->quoteTableName($viewName) . ' AS ' . $subQuery;
Expand Down
2 changes: 2 additions & 0 deletions src/QueryBuilder/AbstractDQLQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Yiisoft\Db\Expression\ArrayExpression;
use Yiisoft\Db\Expression\ArrayExpressionBuilder;
use Yiisoft\Db\Expression\Expression;
use Yiisoft\Db\Expression\ExpressionBuilder;
use Yiisoft\Db\Expression\ExpressionBuilderInterface;
use Yiisoft\Db\Expression\ExpressionInterface;
use Yiisoft\Db\Expression\JsonExpression;
Expand Down Expand Up @@ -522,6 +523,7 @@ protected function defaultExpressionBuilders(): array
return [
Query::class => QueryExpressionBuilder::class,
Param::class => ParamBuilder::class,
Expression::class => ExpressionBuilder::class,
Condition\AbstractConjunctionCondition::class => Condition\Builder\ConjunctionConditionBuilder::class,
Condition\NotCondition::class => Condition\Builder\NotConditionBuilder::class,
Condition\AndCondition::class => Condition\Builder\ConjunctionConditionBuilder::class,
Expand Down
59 changes: 55 additions & 4 deletions src/QueryBuilder/AbstractQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@
use Yiisoft\Db\Schema\Column\ColumnFactoryInterface;
use Yiisoft\Db\Schema\Column\ColumnInterface;
use Yiisoft\Db\Schema\QuoterInterface;
use Yiisoft\Db\Syntax\AbstractSqlParser;

use function bin2hex;
use function count;
use function get_resource_type;
use function gettype;
use function is_string;
use function stream_get_contents;
use function strlen;
use function substr_replace;

/**
* Builds a SELECT SQL statement based on the specification given as a {@see QueryInterface} object.
Expand Down Expand Up @@ -398,7 +401,7 @@ public function prepareParam(ParamInterface $param): string

public function prepareValue(mixed $value): string
{
$quoter = $this->db->getQuoter();
$params = [];

/** @psalm-suppress MixedArgument */
return match (gettype($value)) {
Expand All @@ -407,13 +410,16 @@ public function prepareValue(mixed $value): string
GettypeResult::INTEGER => (string) $value,
GettypeResult::NULL => 'NULL',
GettypeResult::OBJECT => match (true) {
$value instanceof Expression => (string) $value,
$value instanceof ParamInterface => $this->prepareParam($value),
default => $quoter->quoteValue((string) $value),
$value instanceof ExpressionInterface => $this->replacePlaceholders(
$this->buildExpression($value, $params),
array_map($this->prepareValue(...), $params),
),
default => $this->db->getQuoter()->quoteValue((string) $value),
},
GettypeResult::RESOURCE => $this->prepareResource($value),
GettypeResult::RESOURCE_CLOSED => throw new InvalidArgumentException('Resource is closed.'),
default => $quoter->quoteValue((string) $value),
default => $this->db->getQuoter()->quoteValue((string) $value),
};
}

Expand All @@ -427,6 +433,42 @@ public function renameTable(string $oldName, string $newName): string
return $this->ddlBuilder->renameTable($oldName, $newName);
}

public function replacePlaceholders(string $sql, array $replacements): string
{
if (isset($replacements[0])) {
return $sql;
}

/** @psalm-var array<string, string> $replacements */
foreach ($replacements as $placeholder => $replacement) {
if ($placeholder[0] !== ':') {
unset($replacements[$placeholder]);
$replacements[":$placeholder"] = $replacement;
}
}

$offset = 0;
$parser = $this->createSqlParser($sql);

while (null !== $placeholder = $parser->getNextPlaceholder($position)) {
if (isset($replacements[$placeholder])) {
$replacement = $replacements[$placeholder];

/** @var int $position */
$sql = substr_replace($sql, $replacement, $position + $offset, strlen($placeholder));

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

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

return $sql;
}

public function resetSequence(string $table, int|string|null $value = null): string
{
return $this->dmlBuilder->resetSequence($table, $value);
Expand Down Expand Up @@ -478,6 +520,15 @@ public function withTypecasting(bool $typecasting = true): static
return $new;
}

/**
* 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;

/**
* Converts a resource value to its SQL representation or throws an exception if conversion is not possible.
*
Expand Down
2 changes: 1 addition & 1 deletion src/QueryBuilder/DQLQueryBuilderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ interface DQLQueryBuilderInterface
* to the SQL statement (the second array element). The parameters returned include those provided in `$params`.
*
* @psalm-param ParamsType $params
* @psalm-return array{0: string, 1: array}
* @psalm-return array{0: string, 1: ParamsType}
*/
public function build(QueryInterface $query, array $params = []): array;

Expand Down
12 changes: 12 additions & 0 deletions src/QueryBuilder/QueryBuilderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,16 @@ public function prepareParam(ParamInterface $param): string;
* Used when the bind parameter cannot be used in the SQL query.
*/
public function prepareValue(mixed $value): string;

/**
* Replaces placeholders in the SQL string with the corresponding values.
*
* @param string $sql SQL expression where the placeholder should be replaced.
* @param string[] $replacements Replacements for placeholders with placeholder names as keys and values as follows:
* - quoted string values (name => value) use {@see prepareValue()} to prepare the values;
* - new placeholder names prefixed with colon `:` (name => new name).
*
* @return string SQL expression with replaced placeholders.
*/
public function replacePlaceholders(string $sql, array $replacements): string;
}
26 changes: 26 additions & 0 deletions tests/AbstractQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2006,6 +2006,32 @@ public function testRenameTable(): void
);
}

public function testReplacePlaceholders(): void
{
$db = $this->getConnection();

$qb = $db->getQueryBuilder();
$sql = $qb->replacePlaceholders(
'SELECT * FROM [[table]] WHERE [[id]] = :id AND [[name]] = :name AND [[is_active]] = :is_active AND [[created_at]] = :created_at',
[
':id' => '1',
'name' => "'John'",
':is_active' => ':active',
],
);

$this->assertSame(
"SELECT * FROM [[table]] WHERE [[id]] = 1 AND [[name]] = 'John' AND [[is_active]] = :active AND [[created_at]] = :created_at",
$sql,
);

// Question mark placeholder are not replaced
$this->assertSame(
'SELECT * FROM [[table]] WHERE [[id]] = ?',
$qb->replacePlaceholders('SELECT * FROM [[table]] WHERE [[id]] = ?', ['1']),
);
}

/**
* @throws Exception
* @throws NotSupportedException
Expand Down
4 changes: 4 additions & 0 deletions tests/Provider/QueryBuilderProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -1752,6 +1752,9 @@ public static function prepareParam(): array
'float' => ['1.1', 1.1, DataType::STRING],
'string' => ["'string'", 'string', DataType::STRING],
'binary' => ['0x737472696e67', 'string', DataType::LOB],
'expression' => ['(1 + 2)', new Expression('(1 + 2)'), DataType::STRING],
'expression with params' => ['(1 + 2)', new Expression('(:a + :b)', [':a' => 1, 'b' => 2]), DataType::STRING],
'Stringable' => ["'string'", new Stringable('string'), DataType::STRING],
];
}

Expand All @@ -1769,6 +1772,7 @@ public static function prepareValue(): array
'paramString' => ["'string'", new Param('string', DataType::STRING)],
'paramInteger' => ['1', new Param(1, DataType::INTEGER)],
'expression' => ['(1 + 2)', new Expression('(1 + 2)')],
'expression with params' => ['(1 + 2)', new Expression('(:a + :b)', [':a' => 1, 'b' => 2])],
'Stringable' => ["'string'", new Stringable('string')],
];
}
Expand Down
8 changes: 0 additions & 8 deletions tests/Support/Stub/DQLQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,8 @@

namespace Yiisoft\Db\Tests\Support\Stub;

use Yiisoft\Db\Expression\Expression;
use Yiisoft\Db\QueryBuilder\AbstractDQLQueryBuilder;

final class DQLQueryBuilder extends AbstractDQLQueryBuilder
{
protected function defaultExpressionBuilders(): array
{
return [
...parent::defaultExpressionBuilders(),
Expression::class => ExpressionBuilder::class,
];
}
}
5 changes: 5 additions & 0 deletions tests/Support/Stub/QueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,9 @@ public function __construct(ConnectionInterface $db)
new ColumnDefinitionBuilder($this),
);
}

protected function createSqlParser(string $sql): SqlParser
{
return new SqlParser($sql);
}
}