Passed
Pull Request — master (#70)
by Wilmer
16:10
created

QueryBuilder::batchInsert()   C

Complexity

Conditions 14
Paths 97

Size

Total Lines 68
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 14.4642

Importance

Changes 0
Metric Value
cc 14
eloc 32
c 0
b 0
f 0
nc 97
nop 4
dl 0
loc 68
ccs 13
cts 15
cp 0.8667
crap 14.4642
rs 6.2666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Pgsql;
6
7
use Generator;
8
use JsonException;
9
use PDO;
10
use Yiisoft\Db\Constraint\Constraint;
11
use Yiisoft\Db\Exception\Exception;
12
use Yiisoft\Db\Exception\InvalidArgumentException;
13
use Yiisoft\Db\Exception\InvalidConfigException;
14
use Yiisoft\Db\Exception\NotSupportedException;
15
use Yiisoft\Db\Expression\ArrayExpression;
16
use Yiisoft\Db\Expression\Expression;
17
use Yiisoft\Db\Expression\ExpressionInterface;
18
use Yiisoft\Db\Expression\JsonExpression;
19
use Yiisoft\Db\Pdo\PdoValue;
20
use Yiisoft\Db\Query\Conditions\LikeCondition;
21
use Yiisoft\Db\Query\Query;
22
use Yiisoft\Db\Query\QueryBuilder as AbstractQueryBuilder;
23
use Yiisoft\Db\Schema\ColumnSchemaBuilder;
24
use Yiisoft\Strings\NumericHelper;
25
26
use function array_diff;
27
use function array_merge;
28
use function array_unshift;
29
use function explode;
30
use function implode;
31
use function is_bool;
32
use function is_float;
33
use function is_string;
34
use function preg_match;
35
use function preg_replace;
36
use function reset;
37
use function strpos;
38
39
final class QueryBuilder extends AbstractQueryBuilder
40
{
41
    /**
42
     * Defines a UNIQUE index for {@see createIndex()}.
43
     */
44
    public const INDEX_UNIQUE = 'unique';
45
46
    /**
47
     * Defines a B-tree index for {@see createIndex()}.
48
     */
49
    public const INDEX_B_TREE = 'btree';
50
51
    /**
52
     * Defines a hash index for {@see createIndex()}.
53
     */
54
    public const INDEX_HASH = 'hash';
55
56
    /**
57
     * Defines a GiST index for {@see createIndex()}.
58
     */
59
    public const INDEX_GIST = 'gist';
60
61
    /**
62
     * Defines a GIN index for {@see createIndex()}.
63
     */
64
    public const INDEX_GIN = 'gin';
65
66
    /**
67
     * @var array mapping from abstract column types (keys) to physical column types (values).
68
     */
69
    protected array $typeMap = [
70
        Schema::TYPE_PK => 'serial NOT NULL PRIMARY KEY',
71
        Schema::TYPE_UPK => 'serial NOT NULL PRIMARY KEY',
72
        Schema::TYPE_BIGPK => 'bigserial NOT NULL PRIMARY KEY',
73
        Schema::TYPE_UBIGPK => 'bigserial NOT NULL PRIMARY KEY',
74
        Schema::TYPE_CHAR => 'char(1)',
75
        Schema::TYPE_STRING => 'varchar(255)',
76
        Schema::TYPE_TEXT => 'text',
77
        Schema::TYPE_TINYINT => 'smallint',
78
        Schema::TYPE_SMALLINT => 'smallint',
79
        Schema::TYPE_INTEGER => 'integer',
80
        Schema::TYPE_BIGINT => 'bigint',
81
        Schema::TYPE_FLOAT => 'double precision',
82
        Schema::TYPE_DOUBLE => 'double precision',
83
        Schema::TYPE_DECIMAL => 'numeric(10,0)',
84
        Schema::TYPE_DATETIME => 'timestamp(0)',
85
        Schema::TYPE_TIMESTAMP => 'timestamp(0)',
86
        Schema::TYPE_TIME => 'time(0)',
87
        Schema::TYPE_DATE => 'date',
88
        Schema::TYPE_BINARY => 'bytea',
89
        Schema::TYPE_BOOLEAN => 'boolean',
90
        Schema::TYPE_MONEY => 'numeric(19,4)',
91
        Schema::TYPE_JSON => 'jsonb',
92
    ];
93
94
    /**
95
     * Contains array of default condition classes. Extend this method, if you want to change default condition classes
96
     * for the query builder.
97
     *
98
     * @return array
99
     *
100
     * See {@see conditionClasses} docs for details.
101
     */
102
    protected function defaultConditionClasses(): array
103
    {
104
        return array_merge(parent::defaultConditionClasses(), [
105 305
            'ILIKE' => LikeCondition::class,
106
            'NOT ILIKE' => LikeCondition::class,
107 305
            'OR ILIKE' => LikeCondition::class,
108 305
            'OR NOT ILIKE' => LikeCondition::class,
109
        ]);
110
    }
111
112
    /**
113
     * Contains array of default expression builders. Extend this method and override it, if you want to change default
114
     * expression builders for this query builder.
115
     *
116
     * @return array
117
     *
118
     * See {@see ExpressionBuilder} docs for details.
119
     */
120
    protected function defaultExpressionBuilders(): array
121
    {
122
        return array_merge(parent::defaultExpressionBuilders(), [
123 305
            ArrayExpression::class => ArrayExpressionBuilder::class,
124
            JsonExpression::class => JsonExpressionBuilder::class,
125 305
        ]);
126 305
    }
127
128
    /**
129
     * Builds a SQL statement for creating a new index.
130
     *
131
     * @param string $name the name of the index. The name will be properly quoted by the method.
132
     * @param string $table the table that the new index will be created for. The table name will be properly quoted by
133
     * the method.
134
     * @param array|string $columns the column(s) that should be included in the index. If there are multiple columns,
135
     * separate them with commas or use an array to represent them. Each column name will be properly quoted by the
136
     * method, unless a parenthesis is found in the name.
137
     * @param bool|string $unique whether to make this a UNIQUE index constraint. You can pass `true` or
138
     * {@see INDEX_UNIQUE} to create a unique index, `false` to make a non-unique index using the default index type, or
139
     * one of the following constants to specify the index method to use: {@see INDEX_B_TREE}, {@see INDEX_HASH},
140
     * {@see INDEX_GIST}, {@see INDEX_GIN}.
141
     *
142
     * @throws Exception|InvalidArgumentException
143
     *
144
     * @return string the SQL statement for creating a new index.
145
     *
146
     * {@see http://www.postgresql.org/docs/8.2/static/sql-createindex.html}
147
     */
148
    public function createIndex(string $name, string $table, $columns, $unique = false): string
149
    {
150
        if ($unique === self::INDEX_UNIQUE || $unique === true) {
151 5
            $index = false;
152
            $unique = true;
153 5
        } else {
154 3
            $index = $unique;
155 3
            $unique = false;
156
        }
157 3
158 3
        return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ')
159
            . $this->getDb()->quoteTableName($name) . ' ON '
160
            . $this->getDb()->quoteTableName($table)
161 5
            . ($index !== false ? " USING $index" : '')
162 5
            . ' (' . $this->buildColumns($columns) . ')';
163 5
    }
164 5
165 5
    /**
166
     * Builds a SQL statement for dropping an index.
167
     *
168
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
169
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
170
     *
171
     * @return string the SQL statement for dropping an index.
172
     */
173
    public function dropIndex(string $name, string $table): string
174
    {
175
        if (strpos($table, '.') !== false && strpos($name, '.') === false) {
176 3
            if (strpos($table, '{{') !== false) {
177
                $table = preg_replace('/{{(.*?)}}/', '\1', $table);
178 3
                [$schema, $table] = explode('.', $table);
179 1
                if (strpos($schema, '%') === false) {
180 1
                    $name = $schema . '.' . $name;
181 1
                } else {
182 1
                    $name = '{{' . $schema . '.' . $name . '}}';
183 1
                }
184
            } else {
185 1
                [$schema] = explode('.', $table);
186
                $name = $schema . '.' . $name;
187
            }
188
        }
189
190
        return 'DROP INDEX ' . $this->getDb()->quoteTableName($name);
191
    }
192
193 3
    /**
194
     * Builds a SQL statement for renaming a DB table.
195
     *
196
     * @param string $oldName the table to be renamed. The name will be properly quoted by the method.
197
     * @param string $newName the new table name. The name will be properly quoted by the method.
198
     *
199
     * @return string the SQL statement for renaming a DB table.
200
     */
201
    public function renameTable(string $oldName, string $newName): string
202
    {
203
        return 'ALTER TABLE ' . $this->getDb()->quoteTableName($oldName) . ' RENAME TO '
204 2
            . $this->getDb()->quoteTableName($newName);
205
    }
206 2
207 2
    /**
208
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
209
     *
210
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
211
     * or 1.
212
     *
213
     * @param string $tableName the name of the table whose primary key sequence will be reset.
214
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, the next new
215
     * row's primary key will have a value 1.
216
     *
217
     * @throws Exception|InvalidArgumentException|JsonException if the table does not exist or there is no sequence
218
     * associated with the table.
219
     *
220
     * @return string the SQL statement for resetting sequence.
221
     *
222
     * @psalm-suppress MixedArgument
223
     */
224
    public function resetSequence(string $tableName, $value = null): string
225 1
    {
226
        $table = $this->getDb()->getTableSchema($tableName);
227 1
228 1
        if ($table !== null && ($sequence = $table->getSequenceName()) !== null) {
229
            /**
230
             * {@see http://www.postgresql.org/docs/8.1/static/functions-sequence.html}
231
             */
232 1
            $sequence = $this->getDb()->quoteTableName($sequence);
233 1
            $tableName = $this->getDb()->quoteTableName($tableName);
234 1
235 1
            if ($value === null) {
236 1
                $pk = $table->getPrimaryKey();
237 1
                $key = $this->getDb()->quoteColumnName(reset($pk));
238
                $value = "(SELECT COALESCE(MAX({$key}),0) FROM {$tableName})+1";
239 1
            } else {
240
                $value = (int) $value;
241
            }
242 1
243
            return "SELECT SETVAL('$sequence',$value,false)";
244
        }
245
246
        if ($table === null) {
247
            throw new InvalidArgumentException("Table not found: $tableName");
248
        }
249
250
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.");
251
    }
252
253
    /**
254
     * Builds a SQL statement for enabling or disabling integrity check.
255
     *
256
     * @param string $schema the schema of the tables.
257
     * @param string $table the table name.
258
     * @param bool $check whether to turn on or off the integrity check.
259
     *
260
     * @throws Exception|NotSupportedException
261
     *
262
     * @return string the SQL statement for checking integrity.
263
     */
264
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
265
    {
266
        /** @psalm-var Connection $db */
267
        $db = $this->getDb();
268
269
        $enable = $check ? 'ENABLE' : 'DISABLE';
270
        $schema = $schema ?: $db->getSchema()->getDefaultSchema();
271
        $tableNames = [];
272
        $viewNames = [];
273
274
        if ($schema !== null) {
275
            $tableNames = $table ? [$table] : $db->getSchema()->getTableNames($schema);
276
            $viewNames = $db->getSchema()->getViewNames($schema);
277
        }
278
279
        $tableNames = array_diff($tableNames, $viewNames);
280
        $command = '';
281
282
        foreach ($tableNames as $tableName) {
283
            $tableName = $db->quoteTableName("{$schema}.{$tableName}");
284
            $command .= "ALTER TABLE $tableName $enable TRIGGER ALL; ";
285
        }
286
287
        /** enable to have ability to alter several tables */
288
        $db->getMasterPdo()->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
289
290
        return $command;
291
    }
292
293 1
    /**
294
     * Builds a SQL statement for truncating a DB table.
295 1
     *
296
     * Explicitly restarts identity for PGSQL to be consistent with other databases which all do this by default.
297
     *
298
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
299
     *
300
     * @return string the SQL statement for truncating a DB table.
301
     */
302
    public function truncateTable(string $table): string
303
    {
304
        return 'TRUNCATE TABLE ' . $this->getDb()->quoteTableName($table) . ' RESTART IDENTITY';
305
    }
306
307
    /**
308
     * Builds a SQL statement for changing the definition of a column.
309
     *
310
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the
311 1
     * method.
312
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
313 1
     * @param ColumnSchemaBuilder|string $type the new column type. The {@see getColumnType()} method will be invoked to
314 1
     * convert abstract column type (if any) into the physical one. Anything that is not recognized as abstract type
315
     * will be kept in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while
316
     * 'string not null' will become 'varchar(255) not null'. You can also use PostgreSQL-specific syntax such as
317
     * `SET NOT NULL`.
318
     *
319
     * @return string the SQL statement for changing the definition of a column.
320 1
     */
321 1
    public function alterColumn(string $table, string $column, $type): string
322
    {
323
        $columnName = $this->getDb()->quoteColumnName($column);
324 1
        $tableName = $this->getDb()->quoteTableName($table);
325 1
326 1
        if (is_object($type)) {
327
            /** @var string $type */
328 1
            $type = $type->__toString();
329 1
        }
330 1
331
        /**
332
         * {@see https://github.com/yiisoft/yii2/issues/4492}
333 1
         * {@see http://www.postgresql.org/docs/9.1/static/sql-altertable.html}
334
         */
335 1
        if (preg_match('/^(DROP|SET|RESET)\s+/i', $type)) {
336 1
            return "ALTER TABLE {$tableName} ALTER COLUMN {$columnName} {$type}";
337
        }
338
339 1
        $type = 'TYPE ' . $this->getColumnType($type);
340 1
341 1
        $multiAlterStatement = [];
342
        $constraintPrefix = preg_replace('/[^a-z0-9_]/i', '', $table . '_' . $column);
343
344
        if (preg_match('/\s+DEFAULT\s+(["\']?\w*["\']?)/i', $type, $matches)) {
345 1
            $type = preg_replace('/\s+DEFAULT\s+(["\']?\w*["\']?)/i', '', $type);
346 1
            $multiAlterStatement[] = "ALTER COLUMN {$columnName} SET DEFAULT {$matches[1]}";
347 1
        } else {
348
            // safe to drop default even if there was none in the first place
349
            $multiAlterStatement[] = "ALTER COLUMN {$columnName} DROP DEFAULT";
350 1
        }
351
352 1
        $type = preg_replace('/\s+NOT\s+NULL/i', '', $type, -1, $count);
353 1
354
        if ($count) {
355
            $multiAlterStatement[] = "ALTER COLUMN {$columnName} SET NOT NULL";
356
        } else {
357 1
            // remove additional null if any
358
            $type = preg_replace('/\s+NULL/i', '', $type);
359 1
            // safe to drop not null even if there was none in the first place
360
            $multiAlterStatement[] = "ALTER COLUMN {$columnName} DROP NOT NULL";
361
        }
362
363
        if (preg_match('/\s+CHECK\s+\((.+)\)/i', $type, $matches)) {
364
            $type = preg_replace('/\s+CHECK\s+\((.+)\)/i', '', $type);
365
            $multiAlterStatement[] = "ADD CONSTRAINT {$constraintPrefix}_check CHECK ({$matches[1]})";
366
        }
367
368
        $type = preg_replace('/\s+UNIQUE/i', '', $type, -1, $count);
369
        if ($count) {
370
            $multiAlterStatement[] = "ADD UNIQUE ({$columnName})";
371
        }
372
373
        // add what's left at the beginning
374
        array_unshift($multiAlterStatement, "ALTER COLUMN {$columnName} {$type}");
375
376
        return 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $multiAlterStatement);
377
    }
378
379
    /**
380
     * Creates an INSERT SQL statement.
381
     *
382
     * For example,.
383
     *
384
     * ```php
385
     * $sql = $queryBuilder->insert('user', [
386
     *     'name' => 'Sam',
387
     *     'age' => 30,
388
     * ], $params);
389
     * ```
390 43
     *
391
     * The method will properly escape the table and column names.
392 43
     *
393
     * @param string $table the table that new rows will be inserted into.
394
     * @param array|Query $columns the column data (name => value) to be inserted into the table or instance of
395
     * {@see Query|Query} to perform INSERT INTO ... SELECT SQL statement. Passing of
396
     * {@see Query|Query}.
397
     * @param array $params the binding parameters that will be generated by this method. They should be bound to the
398
     * DB command later.
399
     *
400
     * @throws Exception|InvalidArgumentException|InvalidConfigException|JsonException|NotSupportedException
401
     *
402
     * @return string the INSERT SQL
403
     *
404
     * @psalm-suppress MixedArgument
405
     */
406
    public function insert(string $table, $columns, array &$params = []): string
407
    {
408
        return parent::insert($table, $this->normalizeTableRowData($table, $columns), $params);
409
    }
410
411
    /**
412
     * Creates an SQL statement to insert rows into a database table if they do not already exist (matching unique
413
     * constraints), or update them if they do.
414
     *
415
     * For example,
416
     *
417
     * ```php
418
     * $sql = $queryBuilder->upsert('pages', [
419
     *     'name' => 'Front page',
420
     *     'url' => 'http://example.com/', // url is unique
421
     *     'visits' => 0,
422
     * ], [
423
     *     'visits' => new \Yiisoft\Db\Expression('visits + 1'),
424
     * ], $params);
425
     * ```
426
     *
427
     * The method will properly escape the table and column names.
428
     *
429
     * @param string $table the table that new rows will be inserted into/updated in.
430 18
     * @param array|Query $insertColumns the column data (name => value) to be inserted into the table or instance of
431
     * {@see Query} to perform `INSERT INTO ... SELECT` SQL statement.
432 18
     * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist.
433
     * If `true` is passed, the column data will be updated to match the insert column data.
434 18
     * If `false` is passed, no update will be performed if the column data already exists.
435 7
     * @param array $params the binding parameters that will be generated by this method.
436
     * They should be bound to the DB command later.
437 18
     *
438
     * @throws Exception|InvalidConfigException|JsonException|NotSupportedException if this is not supported by the
439
     * underlying DBMS.
440
     *
441 18
     * @return string the resulting SQL.
442
     *
443
     * {@see https://www.postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT}
444
     * {@see https://stackoverflow.com/questions/1109061/insert-on-duplicate-update-in-postgresql/8702291#8702291}
445
     *
446
     * @psalm-suppress MixedArgument
447
     * @psalm-suppress MixedAssignment
448
     */
449
    public function upsert(string $table, $insertColumns, $updateColumns, array &$params = []): string
450
    {
451
        $insertColumns = $this->normalizeTableRowData($table, $insertColumns);
452
453
        if (!is_bool($updateColumns)) {
454
            $updateColumns = $this->normalizeTableRowData($table, $updateColumns);
455
        }
456 18
457
        return $this->newUpsert($table, $insertColumns, $updateColumns, $params);
458 18
    }
459 18
460
    /**
461 18
     * {@see upsert()} implementation for PostgreSQL 9.5 or higher.
462 3
     *
463
     * @param string $table
464
     * @param array|Query $insertColumns
465 15
     * @param array|bool $updateColumns
466
     * @param array $params
467
     *
468
     * @throws Exception|InvalidArgumentException|InvalidConfigException|JsonException|NotSupportedException
469
     *
470 15
     * @return string
471 5
     */
472
    private function newUpsert(string $table, $insertColumns, $updateColumns, array &$params = []): string
473
    {
474 10
        $insertSql = $this->insert($table, $insertColumns, $params);
475 4
476 4
        /** @var array<array-key, mixed> $uniqueNames */
477 4
        [$uniqueNames, , $updateNames] = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns);
478
479
        if (empty($uniqueNames)) {
480
            return $insertSql;
481 10
        }
482
483 10
        if ($updateNames === []) {
484 10
            /** there are no columns to update */
485
            $updateColumns = false;
486
        }
487
488
        if ($updateColumns === false) {
489
            return "$insertSql ON CONFLICT DO NOTHING";
490
        }
491
492
        if ($updateColumns === true) {
493
            $updateColumns = [];
494
495
            /** @var string $name */
496
            foreach ($updateNames as $name) {
497
                $updateColumns[$name] = new Expression('EXCLUDED.' . $this->getDb()->quoteColumnName($name));
498
            }
499
        }
500
501
        /** @var array<array-key, mixed> $updates */
502
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
503
504
        return $insertSql . ' ON CONFLICT (' . implode(', ', $uniqueNames) . ') DO UPDATE SET '
505
            . implode(', ', $updates);
506
    }
507
508
    /**
509
     * Creates an UPDATE SQL statement.
510
     *
511
     * For example,
512
     *
513
     * ```php
514
     * $params = [];
515
     * $sql = $queryBuilder->update('user', ['status' => 1], 'age > 30', $params);
516
     * ```
517
     *
518
     * The method will properly escape the table and column names.
519
     *
520
     * @param string $table the table to be updated.
521
     * @param array $columns the column data (name => value) to be updated.
522
     * @param array|string $condition the condition that will be put in the WHERE part. Please refer to
523
     * {@see Query::where()} on how to specify condition.
524
     * @param array $params the binding parameters that will be modified by this method so that they can be bound to the
525
     * DB command later.
526
     *
527
     * @throws Exception|InvalidArgumentException|JsonException
528
     *
529
     * @return string the UPDATE SQL.
530
     * @psalm-suppress MixedArgument
531
     */
532
    public function update(string $table, array $columns, $condition, array &$params = []): string
533
    {
534
        return parent::update($table, $this->normalizeTableRowData($table, $columns), $condition, $params);
535
    }
536
537
    /**
538
     * Normalizes data to be saved into the table, performing extra preparations and type converting, if necessary.
539
     *
540
     * @param string $table the table that data will be saved into.
541
     * @param array|Query $columns the column data (name => value) to be saved into the table or instance of
542
     * {@see Query} to perform INSERT INTO ... SELECT SQL statement. Passing of
543
     * {@see Query}.
544
     *
545
     * @return mixed normalized columns.
546
     */
547
    private function normalizeTableRowData(string $table, $columns)
548
    {
549
        if ($columns instanceof Query) {
550
            return $columns;
551
        }
552
553
        if (($tableSchema = $this->getDb()->getSchema()->getTableSchema($table)) !== null) {
554
            $columnSchemas = $tableSchema->getColumns();
555
            /** @var mixed $value */
556
            foreach ($columns as $name => $value) {
557
                if (
558
                    isset($columnSchemas[$name]) &&
559
                    $columnSchemas[$name]->getType() === Schema::TYPE_BINARY &&
560
                    is_string($value)
561
                ) {
562
                    /** explicitly setup PDO param type for binary column */
563
                    $columns[$name] = new PdoValue($value, PDO::PARAM_LOB);
564
                }
565
            }
566
        }
567
568
        return $columns;
569
    }
570
571
    /**
572
     * Generates a batch INSERT SQL statement.
573
     *
574
     * For example,
575
     *
576
     * ```php
577
     * $sql = $queryBuilder->batchInsert('user', ['name', 'age'], [
578
     *     ['Tom', 30],
579
     *     ['Jane', 20],
580
     *     ['Linda', 25],
581
     * ]);
582
     * ```
583
     *
584
     * Note that the values in each row must match the corresponding column names.
585
     *
586
     * The method will properly escape the column names, and quote the values to be inserted.
587
     *
588
     * @param string $table the table that new rows will be inserted into.
589
     * @param array<array-key, string> $columns the column names.
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key, string> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key, string>.
Loading history...
590
     * @param array|Generator $rows the rows to be batch inserted into the table.
591
     * @param array $params the binding parameters. This parameter exists.
592
     *
593
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
594
     *
595
     * @return string the batch INSERT SQL statement.
596
     * @psalm-suppress MoreSpecificImplementedParamType
597
     */
598
    public function batchInsert(string $table, array $columns, $rows, array &$params = []): string
599
    {
600
        if (empty($rows)) {
601
            return '';
602
        }
603
604
        /**
605
         * @var array<array-key, object> $columnSchemas
606
         */
607
        $columnSchemas = [];
608
        $schema = $this->getDb()->getSchema();
609
610
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
611
            $columnSchemas = $tableSchema->getColumns();
612
        }
613
614
        $values = [];
615
616
        /**
617
         * @var array<array-key, mixed> $row
618
         */
619
        foreach ($rows as $row) {
620
            $vs = [];
621
            /**
622
             *  @var int $i
623
             *  @var mixed $value
624
             */
625
            foreach ($row as $i => $value) {
626 4
                if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
627
                    /**
628 4
                     * @var bool|ExpressionInterface|float|int|string|null $value
629
                     * @psalm-suppress MixedMethodCall
630
                     */
631
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
632
                }
633
634
                if (is_string($value)) {
635
                    $value = $schema->quoteValue($value);
636
                } elseif (is_float($value)) {
637
                    /** ensure type cast always has . as decimal separator in all locales */
638
                    $value = NumericHelper::normalize((string) $value);
639
                } elseif ($value === true) {
640
                    $value = 'TRUE';
641 45
                } elseif ($value === false) {
642
                    $value = 'FALSE';
643 45
                } elseif ($value === null) {
644 14
                    $value = 'NULL';
645
                } elseif ($value instanceof ExpressionInterface) {
646
                    $value = $this->buildExpression($value, $params);
647 37
                }
648 37
649 37
                /** @var bool|ExpressionInterface|float|int|string|null $value */
650
                $vs[] = $value;
651 37
            }
652 37
            $values[] = '(' . implode(', ', $vs) . ')';
653 37
        }
654
655
        if (empty($values)) {
656 1
            return '';
657
        }
658
659
        /** @var string name */
660
        foreach ($columns as $i => $name) {
661 37
            $columns[$i] = $schema->quoteColumnName($name);
662
        }
663
664
        return 'INSERT INTO ' . $schema->quoteTableName($table)
665
            . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values);
666
    }
667
}
668