diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cc2faabf..1730ec2d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 2.0.0 under development +- Enh #820: Support `Traversable` values for `AbstractDMLQueryBuilder::batchInsert()` method with empty columns (@Tigrov) - 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) diff --git a/src/QueryBuilder/AbstractDMLQueryBuilder.php b/src/QueryBuilder/AbstractDMLQueryBuilder.php index 302153c6b..1a20cb8cd 100644 --- a/src/QueryBuilder/AbstractDMLQueryBuilder.php +++ b/src/QueryBuilder/AbstractDMLQueryBuilder.php @@ -4,6 +4,8 @@ namespace Yiisoft\Db\QueryBuilder; +use Iterator; +use IteratorAggregate; use JsonException; use Traversable; use Yiisoft\Db\Connection\ConnectionInterface; @@ -62,6 +64,10 @@ public function __construct( public function batchInsert(string $table, array $columns, iterable $rows, array &$params = []): string { + if (!is_array($rows)) { + $rows = $this->prepareTraversable($rows); + } + if (empty($rows)) { return ''; } @@ -69,10 +75,6 @@ public function batchInsert(string $table, array $columns, iterable $rows, array $columns = $this->extractColumnNames($rows, $columns); $values = $this->prepareBatchInsertValues($table, $rows, $columns, $params); - if (empty($values)) { - return ''; - } - $query = 'INSERT INTO ' . $this->quoter->quoteTableName($table); if (count($columns) > 0) { @@ -130,6 +132,29 @@ public function upsert( throw new NotSupportedException(__METHOD__ . ' is not supported by this DBMS.'); } + /** + * Prepare traversable for batch insert. + * + * @param Traversable $rows The rows to be batch inserted into the table. + * + * @return array|Iterator The prepared rows. + * + * @psalm-return Iterator|array> + */ + final protected function prepareTraversable(Traversable $rows): Iterator|array + { + while ($rows instanceof IteratorAggregate) { + $rows = $rows->getIterator(); + } + + /** @var Iterator $rows */ + if (!$rows->valid()) { + return []; + } + + return $rows; + } + /** * Prepare values for batch insert. * @@ -180,21 +205,27 @@ protected function prepareBatchInsertValues(string $table, iterable $rows, array /** * Extract column names from columns and rows. * - * @param string $table The column schemas. - * @param iterable $rows The rows to be batch inserted into the table. + * @param array[]|Iterator $rows The rows to be batch inserted into the table. * @param string[] $columns The column names. * * @return string[] The column names. + * + * @psalm-param Iterator|non-empty-array> $rows */ - protected function extractColumnNames(iterable $rows, array $columns): array + protected function extractColumnNames(array|Iterator $rows, array $columns): array { $columns = $this->getNormalizeColumnNames('', $columns); - if ($columns !== [] || !is_array($rows)) { + if (!empty($columns)) { return $columns; } - $row = reset($rows); + if ($rows instanceof Iterator) { + $row = $rows->current(); + } else { + $row = reset($rows); + } + $row = match (true) { is_array($row) => $row, $row instanceof Traversable => iterator_to_array($row), diff --git a/src/QueryBuilder/DMLQueryBuilderInterface.php b/src/QueryBuilder/DMLQueryBuilderInterface.php index 24aa4c856..e00113a57 100644 --- a/src/QueryBuilder/DMLQueryBuilderInterface.php +++ b/src/QueryBuilder/DMLQueryBuilderInterface.php @@ -45,7 +45,7 @@ interface DMLQueryBuilderInterface * @return string The batch INSERT SQL statement. * * @psalm-param string[] $columns - * @psalm-param iterable> $rows + * @psalm-param iterable> $rows * @psalm-param ParamsType $params * * Note: diff --git a/tests/Common/CommonCommandTest.php b/tests/Common/CommonCommandTest.php index 924a94800..5b00419ef 100644 --- a/tests/Common/CommonCommandTest.php +++ b/tests/Common/CommonCommandTest.php @@ -306,7 +306,7 @@ public function testAddUnique(string $name, string $tableName, array|string $col public function testBatchInsert( string $table, array $columns, - array $values, + iterable $values, string $expected, array $expectedParams = [], int $insertedRow = 1 diff --git a/tests/Provider/CommandProvider.php b/tests/Provider/CommandProvider.php index e552165b6..c0c1bee51 100644 --- a/tests/Provider/CommandProvider.php +++ b/tests/Provider/CommandProvider.php @@ -5,6 +5,8 @@ namespace Yiisoft\Db\Tests\Provider; use ArrayIterator; +use IteratorAggregate; +use Traversable; use Yiisoft\Db\Command\DataType; use Yiisoft\Db\Command\Param; use Yiisoft\Db\Expression\Expression; @@ -463,7 +465,7 @@ public static function batchInsert(): array ':qp3' => true, ], ], - 'empty columns and Traversable' => [ + 'empty columns and a Traversable value' => [ 'type', [], 'values' => [new ArrayIterator(['int_col' => '1.0', 'float_col' => '2', 'char_col' => 10, 'bool_col' => 1])], @@ -480,6 +482,28 @@ public static function batchInsert(): array ':qp3' => true, ], ], + 'empty columns and Traversable values' => [ + 'type', + [], + 'values' => new class () implements IteratorAggregate { + public function getIterator(): Traversable + { + yield ['int_col' => '1.0', 'float_col' => '2', 'char_col' => 10, 'bool_col' => 1]; + } + }, + 'expected' => DbHelper::replaceQuotes( + << [ + ':qp0' => 1, + ':qp1' => 2.0, + ':qp2' => '10', + ':qp3' => true, + ], + ], ]; } diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index 28e12116c..8e54c4b33 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -241,7 +241,7 @@ public static function batchInsert(): array 'empty columns and non-exists table' => [ 'non_exists_table', [], - 'values' => [['1.0', '2', 10, 1]], + [['1.0', '2', 10, 1]], 'expected' => DbHelper::replaceQuotes( <<