Skip to content

Commit e7c1cd7

Browse files
committed
Add an insertMany for the Connection class
1 parent d31bcf0 commit e7c1cd7

4 files changed

Lines changed: 227 additions & 0 deletions

File tree

docs/en/reference/data-retrieval-and-manipulation.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,18 @@ data.
530530
$conn->insert('user', ['username' => 'jwage']);
531531
// INSERT INTO user (username) VALUES (?) (jwage)
532532
533+
insertMany()
534+
~~~~~~~~~
535+
536+
Insert multiple rows into the given table name using the key value pairs of
537+
data.
538+
539+
.. code-block:: php
540+
541+
<?php
542+
$conn->insertMany('user', [['username' => 'jwage'], ['username' => 'zezima']]);
543+
// INSERT INTO user (username) VALUES (?), (?) (jwage, zezima)
544+
533545
update()
534546
~~~~~~~~~
535547

src/Connection.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@
3838
use Throwable;
3939
use Traversable;
4040

41+
use function array_fill;
4142
use function array_key_exists;
43+
use function array_keys;
4244
use function array_merge;
4345
use function count;
4446
use function implode;
@@ -517,6 +519,57 @@ public function insert(string $table, array $data, array $types = []): int|strin
517519
);
518520
}
519521

522+
/**
523+
* Inserts multiple table rows with specified data.
524+
*
525+
* Table expression and columns are not escaped and are not safe for user-input.
526+
* Each array within data should have the same keys.
527+
*
528+
* @param array<array<string, mixed>> $data
529+
* @param array<int<0,max>, string|ParameterType|Type>|array<string, string|ParameterType|Type> $types
530+
*
531+
* @return int|numeric-string The number of affected rows.
532+
*
533+
* @throws Exception
534+
*/
535+
public function insertMany(string $table, array $data, array $types = []): int|string
536+
{
537+
$numRows = count($data);
538+
if ($numRows === 0) {
539+
return $this->executeStatement('INSERT INTO ' . $table . ' () VALUES ()');
540+
}
541+
542+
$columns = [];
543+
$values = [];
544+
$set = [];
545+
546+
$first = true;
547+
foreach ($data as $row) {
548+
if ($first) {
549+
$first = false;
550+
$columns = array_keys($row);
551+
$set = array_fill(0, count($columns), '?');
552+
}
553+
554+
foreach ($columns as $column) {
555+
$values[] = $row[$column];
556+
}
557+
}
558+
559+
$setParams = '(' . implode(',', $set) . ')';
560+
561+
$calculatedTypes = is_string(key($types))
562+
? $this->extractTypeValues($columns, $types)
563+
: array_merge($types, array_fill(0, count($columns) - count($types), ParameterType::STRING));
564+
565+
return $this->executeStatement(
566+
'INSERT INTO ' . $table . ' (' . implode(', ', $columns) . ') VALUES '
567+
. implode(', ', array_fill(0, $numRows, $setParams)),
568+
$values,
569+
array_merge(...array_fill(0, $numRows, $calculatedTypes)),
570+
);
571+
}
572+
520573
/**
521574
* Extract ordered type list from an ordered column list and type map.
522575
*

tests/ConnectionTest.php

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,140 @@ public function testEmptyInsert(): void
314314
$conn->insert('footable', []);
315315
}
316316

317+
public function testInsertMany(): void
318+
{
319+
$conn = $this->getExecuteStatementMockConnection();
320+
321+
$conn->expects(self::once())
322+
->method('executeStatement')
323+
->with(
324+
'INSERT INTO footable (c1, c2, c3, c4) VALUES (?,?,?,?), (?,?,?,?)',
325+
[
326+
'i1-c1',
327+
'i1-c2',
328+
'i1-c3',
329+
4,
330+
'i2-c1',
331+
'i2-c2',
332+
'i2-c3',
333+
4,
334+
],
335+
[
336+
'string',
337+
'string',
338+
'string',
339+
'integer',
340+
'string',
341+
'string',
342+
'string',
343+
'integer',
344+
],
345+
);
346+
347+
$conn->insertMany(
348+
'footable',
349+
[
350+
[
351+
'c1' => 'i1-c1',
352+
'c2' => 'i1-c2',
353+
'c3' => 'i1-c3',
354+
'c4' => 4,
355+
],
356+
[
357+
'c3' => 'i2-c3',
358+
'c1' => 'i2-c1',
359+
'c2' => 'i2-c2',
360+
'c4' => 4,
361+
],
362+
363+
],
364+
[
365+
'c3' => 'string',
366+
'c1' => 'string',
367+
'c2' => 'string',
368+
'c4' => 'integer',
369+
],
370+
);
371+
}
372+
373+
public function testInsertManySpecifyingFirstKeyOnly(): void
374+
{
375+
$conn = $this->getExecuteStatementMockConnection();
376+
377+
$conn->expects(self::once())
378+
->method('executeStatement')
379+
->with(
380+
'INSERT INTO footable (c1, c2) VALUES (?,?), (?,?)',
381+
[
382+
1,
383+
'i1-c2',
384+
1,
385+
'i2-c2',
386+
],
387+
[
388+
'integer',
389+
ParameterType::STRING,
390+
'integer',
391+
ParameterType::STRING,
392+
],
393+
);
394+
395+
$conn->insertMany(
396+
'footable',
397+
[
398+
[
399+
'c1' => 1,
400+
'c2' => 'i1-c2',
401+
],
402+
[
403+
'c1' => 1,
404+
'c2' => 'i2-c2',
405+
],
406+
407+
],
408+
['integer'],
409+
);
410+
}
411+
412+
public function testInsertManyWithoutTypes(): void
413+
{
414+
$conn = $this->getExecuteStatementMockConnection();
415+
416+
$conn->expects(self::once())
417+
->method('executeStatement')
418+
->with(
419+
'INSERT INTO footable (c1, c2) VALUES (?,?), (?,?)',
420+
[
421+
1,
422+
'i1-c2',
423+
1,
424+
'i2-c2',
425+
],
426+
[
427+
ParameterType::STRING,
428+
ParameterType::STRING,
429+
ParameterType::STRING,
430+
ParameterType::STRING,
431+
],
432+
);
433+
434+
$conn->insertMany(
435+
'footable',
436+
[
437+
[
438+
'c1' => 1,
439+
'c2' => 'i1-c2',
440+
],
441+
[
442+
'c1' => 1,
443+
'c2' => 'i2-c2',
444+
],
445+
446+
],
447+
[],
448+
);
449+
}
450+
317451
public function testUpdateWithDifferentColumnsInDataAndIdentifiers(): void
318452
{
319453
$conn = $this->getExecuteStatementMockConnection();

tests/Functional/WriteTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,34 @@ public function testDeleteWhereIsNull(): void
373373
self::assertCount(0, $data);
374374
}
375375

376+
public function testInsertMany(): void
377+
{
378+
$count = $this->connection->insertMany(
379+
'write_table',
380+
[
381+
['test_int' => '30', 'test_string' => 'one'],
382+
['test_int' => '20', 'test_string' => 'two'],
383+
['test_string' => 'three', 'test_int' => '10' ],
384+
],
385+
['test_int' => 'integer'],
386+
);
387+
388+
$data = $this->connection->fetchFirstColumn(
389+
'SELECT test_string FROM write_table WHERE test_int IN(10,20,30) ORDER BY test_int',
390+
);
391+
392+
self::assertSame(3, $count);
393+
394+
self::assertEquals(
395+
[
396+
'three',
397+
'two',
398+
'one',
399+
],
400+
$data,
401+
);
402+
}
403+
376404
/** @param class-string<Driver\Exception> $expectedClass */
377405
private function expectDriverException(string $expectedClass, Closure $test): void
378406
{

0 commit comments

Comments
 (0)