Skip to content

Commit

Permalink
Fix of normalize for column names in insert, batchInsert, upsert, upd…
Browse files Browse the repository at this point in the history
…ate (#443)

* Fix of normalize for column names in insert, batchInsert, upsert, update

* remove comments

* styleci
  • Loading branch information
darkdef committed Dec 30, 2022
1 parent 7fac771 commit a53dd36
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 43 deletions.
105 changes: 76 additions & 29 deletions src/QueryBuilder/DMLQueryBuilder.php
Expand Up @@ -42,6 +42,7 @@ public function __construct(
}

/**
* @psalm-param string[] $columns
* @psalm-suppress MixedArrayOffset
*/
public function batchInsert(string $table, array $columns, iterable|Generator $rows, array &$params = []): string
Expand All @@ -56,15 +57,16 @@ public function batchInsert(string $table, array $columns, iterable|Generator $r
$columnSchemas = [];
}

$mappedNames = $this->getNormalizeColumnNames($table, $columns);
$values = [];

/** @psalm-var array<array-key, array<array-key, string>> $rows */
foreach ($rows as $row) {
$placeholders = [];
foreach ($row as $index => $value) {
if (isset($columns[$index], $columnSchemas[$columns[$index]])) {
if (isset($columns[$index], $mappedNames[$columns[$index]], $columnSchemas[$mappedNames[$columns[$index]]])) {
/** @var mixed $value */
$value = $this->getTypecastValue($value, $columnSchemas[$columns[$index]]);
$value = $this->getTypecastValue($value, $columnSchemas[$mappedNames[$columns[$index]]]);
}

if ($value instanceof ExpressionInterface) {
Expand All @@ -80,9 +82,8 @@ public function batchInsert(string $table, array $columns, iterable|Generator $r
return '';
}

/** @psalm-var string[] $columns */
foreach ($columns as $i => $name) {
$columns[$i] = $this->quoter->quoteColumnName($name);
$columns[$i] = $this->quoter->quoteColumnName($mappedNames[$name]);
}

return 'INSERT INTO '
Expand All @@ -103,6 +104,10 @@ public function delete(string $table, array|string $condition, array &$params):

public function insert(string $table, QueryInterface|array $columns, array &$params = []): string
{
if (!$columns instanceof QueryInterface) {
$columns = $this->normalizeColumnNames($table, $columns);
}

/**
* @psalm-var string[] $names
* @psalm-var string[] $placeholders
Expand Down Expand Up @@ -137,6 +142,8 @@ public function resetSequence(string $tableName, int|string|null $value = null):
*/
public function update(string $table, array $columns, array|string $condition, array &$params = []): string
{
$columns = $this->normalizeColumnNames($table, $columns);

/** @psalm-var string[] $lines */
[$lines, $params] = $this->prepareUpdateSets($table, $columns, $params);
$sql = 'UPDATE ' . $this->quoter->quoteTableName($table) . ' SET ' . implode(', ', $lines);
Expand Down Expand Up @@ -342,26 +349,11 @@ static function (Constraint $constraint) {
$columnNames = [];
$quoter = $this->quoter;

// Need get filtered column names. Without name of table. And remove columns from other tables
$unquotedColumns = $simpleColumns = [];
$rawTableName = $this->schema->getRawTableName($name);
foreach ($columns as $column) {
$parts = $quoter->getTableNameParts($column, true);

// Skip columns from other tables
if (count($parts) === 2 && $this->schema->getRawTableName($parts[0]) !== $rawTableName) {
continue;
}

$columnName = $quoter->ensureColumnName($parts[count($parts)-1]);
$unquotedColumns[$column] = $simpleColumns[] = $quoter->quoteColumnName($columnName);
}

// Remove all constraints which do not cover the specified column list.
$constraints = array_values(
array_filter(
$constraints,
static function (Constraint $constraint) use ($quoter, $simpleColumns, &$columnNames) {
static function (Constraint $constraint) use ($quoter, $columns, &$columnNames) {
/** @psalm-var string[]|string $getColumnNames */
$getColumnNames = $constraint->getColumnNames() ?? [];
$constraintColumnNames = [];
Expand All @@ -372,7 +364,7 @@ static function (Constraint $constraint) use ($quoter, $simpleColumns, &$columnN
}
}

$result = !array_diff($constraintColumnNames, $simpleColumns);
$result = !array_diff($constraintColumnNames, $columns);

if ($result) {
$columnNames = array_merge((array) $columnNames, $constraintColumnNames);
Expand All @@ -383,14 +375,8 @@ static function (Constraint $constraint) use ($quoter, $simpleColumns, &$columnN
)
);

// restore original column names
$originalColumnNames = [];
/** @psalm-var string[] $columnNames */
foreach ($columnNames as $columnName) {
$originalColumnNames[] = $unquotedColumns[$columnName] ?? $columnName;
}

return array_unique($originalColumnNames);
/** @psalm-var array $columnNames */
return array_unique($columnNames);
}

protected function getTypecastValue(mixed $value, ColumnSchemaInterface $columnSchema = null): mixed
Expand All @@ -401,4 +387,65 @@ protected function getTypecastValue(mixed $value, ColumnSchemaInterface $columnS

return $value;
}

/**
* Normalizes column names
*
* @param string $table the table that data will be saved into.
* @param array $columns the column data (name => value) to be saved into the table or instance of
* {@see QueryInterface} to perform INSERT INTO ... SELECT SQL statement. Passing of
* {@see QueryInterface}.
*
* @return array normalized columns.
*/
protected function normalizeColumnNames(string $table, array $columns): array
{
/** @var string[] $columnsList */
$columnsList = array_keys($columns);
$mappedNames = $this->getNormalizeColumnNames($table, $columnsList);

/** @psalm-var mixed[] $normalizedColumns */
$normalizedColumns = [];

/**
* @var string $name
* @var mixed $value
*/
foreach ($columns as $name => $value) {
$mappedName = $mappedNames[$name] ?? $name;
/** @psalm-suppress MixedAssignment */
$normalizedColumns[$mappedName] = $value;
}

return $normalizedColumns;
}

/**
* Get map of normalized columns
*
* @param string $table
* @param string[] $columns
*
* @return string[]
*/
protected function getNormalizeColumnNames(string $table, array $columns): array
{
$normalizedNames = [];
$rawTableName = $this->schema->getRawTableName($table);

foreach ($columns as $name) {
$parts = $this->quoter->getTableNameParts($name, true);

if (count($parts) === 2 && $this->schema->getRawTableName($parts[0]) === $rawTableName) {
$normalizedName = $parts[count($parts) - 1];
} else {
$normalizedName = $name;
}
$normalizedName = $this->quoter->ensureColumnName($normalizedName);

$normalizedNames[$name] = $normalizedName;
}

return $normalizedNames;
}
}
2 changes: 1 addition & 1 deletion src/QueryBuilder/DMLQueryBuilderInterface.php
Expand Up @@ -32,7 +32,7 @@ interface DMLQueryBuilderInterface
* The method will properly escape the column names, and quote the values to be inserted.
*
* @param string $table the table that new rows will be inserted into.
* @param array $columns the column names.
* @param string[] $columns the column names.
* @param Generator|iterable $rows the rows to be batched inserted into the table.
* @param array $params the binding parameters. This parameter exists.
*
Expand Down
2 changes: 1 addition & 1 deletion tests/Common/CommonCommandTest.php
Expand Up @@ -278,11 +278,11 @@ public function testBatchInsert(

$command = $db->createCommand();
$command->batchInsert($table, $columns, $values);
$command->prepare(false);

$this->assertSame($expected, $command->getSql());
$this->assertSame($expectedParams, $command->getParams());

$command->prepare(false);
$command->execute();

$this->assertEquals($insertedRow, (new Query($db))->from($table)->count());
Expand Down
3 changes: 1 addition & 2 deletions tests/Provider/AbstractCommandProvider.php
Expand Up @@ -308,11 +308,10 @@ public function batchInsert(): array
*
* In case table name or table column is passed with curly or square bracelets, QueryBuilder can not
* determine the table schema and typecast values properly.
* TODO: make it work. Impossible without BC breaking for public methods.
*/
'expected' => DbHelper::replaceQuotes(
<<<SQL
INSERT INTO [[type]] ([[type]].[[int_col]], [[float_col]], [[char_col]], [[bool_col]]) VALUES (:qp0, :qp1, :qp2, :qp3)
INSERT INTO [[type]] ([[int_col]], [[float_col]], [[char_col]], [[bool_col]]) VALUES (:qp0, :qp1, :qp2, :qp3)
SQL,
$this->getDriverName(),
),
Expand Down
29 changes: 19 additions & 10 deletions tests/Provider/AbstractQueryBuilderProvider.php
Expand Up @@ -187,18 +187,24 @@ public function batchInsert(): array
'{{%type}}',
['{{%type}}.[[float_col]]', '[[time]]'],
[[null, new Expression('now()')], [null, new Expression('now()')]],
'expected' => <<<SQL
INSERT INTO {{%type}} ({{%type}}.[[float_col]], [[time]]) VALUES (:qp0, now()), (:qp1, now())
SQL,
'expected' => DbHelper::replaceQuotes(
<<<SQL
INSERT INTO {{%type}} ([[float_col]], [[time]]) VALUES (:qp0, now()), (:qp1, now())
SQL,
$this->getDriverName()
),
[':qp0' => null, ':qp1' => null],
],
'bool-false, time-now()' => [
'{{%type}}',
['{{%type}}.[[bool_col]]', '[[time]]'],
[[false, new Expression('now()')]],
'expected' => <<<SQL
INSERT INTO {{%type}} ({{%type}}.[[bool_col]], [[time]]) VALUES (:qp0, now())
SQL,
'expected' => DbHelper::replaceQuotes(
<<<SQL
INSERT INTO {{%type}} ([[bool_col]], [[time]]) VALUES (:qp0, now())
SQL,
$this->getDriverName()
),
[':qp0' => null],
],
];
Expand Down Expand Up @@ -899,11 +905,14 @@ public function insert(): array
],
'params-and-expressions' => [
'{{%type}}',
['{{%type}}.[[related_id]]' => null, '[[time]]' => new Expression('now()')],
['{{%type}}.[[related_id]]' => null, 'time' => new Expression('now()')],
[],
<<<SQL
INSERT INTO {{%type}} ({{%type}}.[[related_id]], [[time]]) VALUES (:qp0, now())
SQL,
DbHelper::replaceQuotes(
<<<SQL
INSERT INTO {{%type}} ([[related_id]], [[time]]) VALUES (:qp0, now())
SQL,
$db->getName(),
),
[':qp0' => null],
],
'carry passed params' => [
Expand Down

0 comments on commit a53dd36

Please sign in to comment.