Passed
Pull Request — master (#70)
by Wilmer
27:59 queued 13:00
created

QueryBuilder::batchInsert()   C

Complexity

Conditions 14
Paths 97

Size

Total Lines 67
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 21.257

Importance

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