From 63f68c0337cc2a2754c3e58bcaf717f20e7c5eca Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Sun, 12 Nov 2023 14:20:03 +0700 Subject: [PATCH] Fix `batchInsert()` with associative arrays (#769) * Refactor DMLQueryBuilder * Get uniques using `getTableIndexes()` and `getTableUniques()` * Fix @psalm-var * Fix #61 (point 2) * Fix #61 (point 2) add test * Improve test * Remove methods with `NotSupportedException` * Fix test issues * Fix test issues * Revert "Remove methods with `NotSupportedException`" * Add line to CHANGELOG.md * Change order of checks * Improve performance of quoting column names up to 10% using `array_map()` * Fix `batchInsert()` with associative arrays * Update after merge * Remove old comments * Fix psalm * Add line to CHANGELOG.md --------- Co-authored-by: Sergei Predvoditelev --- CHANGELOG.md | 1 + src/QueryBuilder/AbstractDMLQueryBuilder.php | 37 ++++++----- tests/Provider/CommandProvider.php | 70 +++++++++++++++++++- 3 files changed, 91 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e92f048da..e1ef4bfa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Bug #761: Quote aliases of CTE in `WITH` queries (@Tigrov) - Chg #765: Deprecate `SchemaInterface::TYPE_JSONB` (@Tigrov) - Enh #770: Move methods from concrete `Command` class to `AbstractPdoCommand` class (@Tigrov) +- Bug #769, #61: Fix `AbstractDMLQueryBuilder::batchInsert()` for values as associative arrays (@Tigrov) ## 1.1.1 August 16, 2023 diff --git a/src/QueryBuilder/AbstractDMLQueryBuilder.php b/src/QueryBuilder/AbstractDMLQueryBuilder.php index 1224e503f..5d57d6412 100644 --- a/src/QueryBuilder/AbstractDMLQueryBuilder.php +++ b/src/QueryBuilder/AbstractDMLQueryBuilder.php @@ -19,6 +19,7 @@ use function array_combine; use function array_diff; +use function array_fill_keys; use function array_filter; use function array_keys; use function array_map; @@ -57,25 +58,31 @@ public function batchInsert(string $table, array $columns, iterable $rows, array $values = []; $columns = $this->getNormalizeColumnNames('', $columns); + $columnNames = array_values($columns); + $columnKeys = array_fill_keys($columnNames, false); $columnSchemas = $this->schema->getTableSchema($table)?->getColumns() ?? []; foreach ($rows as $row) { $i = 0; - $placeholders = []; - - foreach ($row as $value) { - if (isset($columns[$i], $columnSchemas[$columns[$i]])) { - $value = $columnSchemas[$columns[$i]]->dbTypecast($value); + $placeholders = $columnKeys; + + foreach ($row as $key => $value) { + /** @psalm-suppress MixedArrayTypeCoercion */ + $columnName = $columns[$key] ?? (isset($columnKeys[$key]) ? $key : $columnNames[$i] ?? $i); + /** @psalm-suppress MixedArrayTypeCoercion */ + if (isset($columnSchemas[$columnName])) { + $value = $columnSchemas[$columnName]->dbTypecast($value); } if ($value instanceof ExpressionInterface) { - $placeholders[] = $this->queryBuilder->buildExpression($value, $params); + $placeholders[$columnName] = $this->queryBuilder->buildExpression($value, $params); } else { - $placeholders[] = $this->queryBuilder->bindParam($value, $params); + $placeholders[$columnName] = $this->queryBuilder->bindParam($value, $params); } ++$i; } + $values[] = '(' . implode(', ', $placeholders) . ')'; } @@ -83,13 +90,13 @@ public function batchInsert(string $table, array $columns, iterable $rows, array return ''; } - $columns = array_map( + $columnNames = array_map( [$this->quoter, 'quoteColumnName'], - $columns, + $columnNames, ); return 'INSERT INTO ' . $this->quoter->quoteTableName($table) - . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values); + . ' (' . implode(', ', $columnNames) . ') VALUES ' . implode(', ', $values); } public function delete(string $table, array|string $condition, array &$params): string @@ -430,13 +437,11 @@ protected function normalizeColumnNames(string $table, array $columns): array */ protected function getNormalizeColumnNames(string $table, array $columns): array { - $normalizedNames = []; - - foreach ($columns as $name) { - $normalizedName = $this->quoter->ensureColumnName($name); - $normalizedNames[] = $this->quoter->unquoteSimpleColumnName($normalizedName); + foreach ($columns as &$name) { + $name = $this->quoter->ensureColumnName($name); + $name = $this->quoter->unquoteSimpleColumnName($name); } - return $normalizedNames; + return $columns; } } diff --git a/tests/Provider/CommandProvider.php b/tests/Provider/CommandProvider.php index 5d2ae6176..9f7408b73 100644 --- a/tests/Provider/CommandProvider.php +++ b/tests/Provider/CommandProvider.php @@ -339,7 +339,7 @@ public static function batchInsert(): array ':qp3' => false, ], ], - 'with associative values' => [ + 'with associative values with different keys' => [ 'type', ['int_col', 'float_col', 'char_col', 'bool_col'], 'values' => [['int' => '1.0', 'float' => '2', 'char' => 10, 'bool' => 1]], @@ -356,6 +356,74 @@ public static function batchInsert(): array ':qp3' => true, ], ], + 'with associative values with different keys and columns with keys' => [ + 'type', + ['a' => 'int_col', 'b' => 'float_col', 'c' => 'char_col', 'd' => 'bool_col'], + 'values' => [['int' => '1.0', 'float' => '2', 'char' => 10, 'bool' => 1]], + 'expected' => DbHelper::replaceQuotes( + << [ + ':qp0' => 1, + ':qp1' => 2.0, + ':qp2' => '10', + ':qp3' => true, + ], + ], + 'with associative values with keys of column names' => [ + 'type', + ['int_col', 'float_col', 'char_col', 'bool_col'], + 'values' => [['bool_col' => 1, 'char_col' => 10, 'int_col' => '1.0', 'float_col' => '2']], + 'expected' => DbHelper::replaceQuotes( + << [ + ':qp0' => true, + ':qp1' => '10', + ':qp2' => 1, + ':qp3' => 2.0, + ], + ], + 'with associative values with keys of column keys' => [ + 'type', + ['int' => 'int_col', 'float' => 'float_col', 'char' => 'char_col', 'bool' => 'bool_col'], + 'values' => [['bool' => 1, 'char' => 10, 'int' => '1.0', 'float' => '2']], + 'expected' => DbHelper::replaceQuotes( + << [ + ':qp0' => true, + ':qp1' => '10', + ':qp2' => 1, + ':qp3' => 2.0, + ], + ], + 'with shuffled indexes of values' => [ + 'type', + ['int_col', 'float_col', 'char_col', 'bool_col'], + 'values' => [[3 => 1, 2 => 10, 0 => '1.0', 1 => '2']], + 'expected' => DbHelper::replaceQuotes( + << [ + ':qp0' => true, + ':qp1' => '10', + ':qp2' => 1, + ':qp3' => 2.0, + ], + ], ]; }