Passed
Push — master ( 640dd0...eecb1b )
by Wilmer
13:16 queued 10:55
created

QueryBuilder::batchInsert()   C

Complexity

Conditions 14
Paths 97

Size

Total Lines 49
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 14.0054

Importance

Changes 0
Metric Value
cc 14
eloc 33
c 0
b 0
f 0
nc 97
nop 4
dl 0
loc 49
ccs 32
cts 33
cp 0.9697
crap 14.0054
rs 6.2666

How to fix   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\Query;
6
7
use PDO;
8
use Yiisoft\Db\Constraint\Constraint;
9
use Yiisoft\Db\Exception\Exception;
10
use Yiisoft\Db\Exception\InvalidArgumentException;
11
use Yiisoft\Db\Exception\InvalidConfigException;
12
use Yiisoft\Db\Exception\NotSupportedException;
13
use Yiisoft\Db\Expression\ArrayExpression;
14
use Yiisoft\Db\Expression\Expression;
15
use Yiisoft\Db\Expression\ExpressionInterface;
16
use Yiisoft\Db\Expression\JsonExpression;
17
use Yiisoft\Db\Pdo\PdoValue;
18
use Yiisoft\Db\Pgsql\Expression\ArrayExpressionBuilder;
19
use Yiisoft\Db\Pgsql\Expression\JsonExpressionBuilder;
20
use Yiisoft\Db\Pgsql\Schema\Schema;
21
use Yiisoft\Db\Query\Conditions\LikeCondition;
22
use Yiisoft\Db\Query\Query;
23
use Yiisoft\Db\Query\QueryBuilder as AbstractQueryBuilder;
24
use Yiisoft\Strings\StringHelper;
25
26
use function array_diff;
27
use function array_merge;
28
use function array_unshift;
29
use function count;
30
use function explode;
31
use function implode;
32
use function is_bool;
33
use function is_float;
34
use function is_string;
35
use function preg_match;
36
use function preg_replace;
37
use function reset;
38
use function strpos;
39
use function version_compare;
40
41
class QueryBuilder extends AbstractQueryBuilder
42
{
43
    /**
44
     * Defines a UNIQUE index for {@see createIndex()}.
45
     */
46
    public const INDEX_UNIQUE = 'unique';
47
48
    /**
49
     * Defines a B-tree index for {@see createIndex()}.
50
     */
51
    public const INDEX_B_TREE = 'btree';
52
53
    /**
54
     * Defines a hash index for {@see createIndex()}.
55
     */
56
    public const INDEX_HASH = 'hash';
57
58
    /**
59
     * Defines a GiST index for {@see createIndex()}.
60
     */
61
    public const INDEX_GIST = 'gist';
62
63
    /**
64
     * Defines a GIN index for {@see createIndex()}.
65
     */
66
    public const INDEX_GIN = 'gin';
67
68
    /**
69
     * @var array mapping from abstract column types (keys) to physical column types (values).
70
     */
71
    protected array $typeMap = [
72
        Schema::TYPE_PK => 'serial NOT NULL PRIMARY KEY',
73
        Schema::TYPE_UPK => 'serial NOT NULL PRIMARY KEY',
74
        Schema::TYPE_BIGPK => 'bigserial NOT NULL PRIMARY KEY',
75
        Schema::TYPE_UBIGPK => 'bigserial NOT NULL PRIMARY KEY',
76
        Schema::TYPE_CHAR => 'char(1)',
77
        Schema::TYPE_STRING => 'varchar(255)',
78
        Schema::TYPE_TEXT => 'text',
79
        Schema::TYPE_TINYINT => 'smallint',
80
        Schema::TYPE_SMALLINT => 'smallint',
81
        Schema::TYPE_INTEGER => 'integer',
82
        Schema::TYPE_BIGINT => 'bigint',
83
        Schema::TYPE_FLOAT => 'double precision',
84
        Schema::TYPE_DOUBLE => 'double precision',
85
        Schema::TYPE_DECIMAL => 'numeric(10,0)',
86
        Schema::TYPE_DATETIME => 'timestamp(0)',
87
        Schema::TYPE_TIMESTAMP => 'timestamp(0)',
88
        Schema::TYPE_TIME => 'time(0)',
89
        Schema::TYPE_DATE => 'date',
90
        Schema::TYPE_BINARY => 'bytea',
91
        Schema::TYPE_BOOLEAN => 'boolean',
92
        Schema::TYPE_MONEY => 'numeric(19,4)',
93
        Schema::TYPE_JSON => 'jsonb',
94
    ];
95
96
    /**
97
     * Contains array of default condition classes. Extend this method, if you want to change default condition classes
98
     * for the query builder.
99
     *
100
     * @return array
101
     *
102
     * See {@see conditionClasses} docs for details.
103
     */
104 276
    protected function defaultConditionClasses(): array
105
    {
106 276
        return array_merge(parent::defaultConditionClasses(), [
107 276
            'ILIKE' => LikeCondition::class,
108
            'NOT ILIKE' => LikeCondition::class,
109
            'OR ILIKE' => LikeCondition::class,
110
            'OR NOT ILIKE' => LikeCondition::class,
111
        ]);
112
    }
113
114
    /**
115
     * Contains array of default expression builders. Extend this method and override it, if you want to change default
116
     * expression builders for this query builder.
117
     *
118
     * @return array
119
     *
120
     * See {@see \Yiisoft\Db\Expression\ExpressionBuilder} docs for details.
121
     */
122 276
    protected function defaultExpressionBuilders(): array
123
    {
124 276
        return array_merge(parent::defaultExpressionBuilders(), [
125 276
            ArrayExpression::class => ArrayExpressionBuilder::class,
126
            JsonExpression::class => JsonExpressionBuilder::class,
127
        ]);
128
    }
129
130
    /**
131
     * Builds a SQL statement for creating a new index.
132
     *
133
     * @param string $name the name of the index. The name will be properly quoted by the method.
134
     * @param string $table the table that the new index will be created for. The table name will be properly quoted by
135
     * the method.
136
     * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns,
137
     * separate them with commas or use an array to represent them. Each column name will be properly quoted by the
138
     * method, unless a parenthesis is found in the name.
139
     * @param bool|string $unique whether to make this a UNIQUE index constraint. You can pass `true` or
140
     * {@see INDEX_UNIQUE} to create a unique index, `false` to make a non-unique index using the default index type, or
141
     * one of the following constants to specify the index method to use: {@see INDEX_B_TREE}, {@see INDEX_HASH},
142
     * {@see INDEX_GIST}, {@see INDEX_GIN}.
143
     *
144
     * @throws Exception
145
     * @throws InvalidArgumentException
146
     * @throws InvalidConfigException
147
     * @throws NotSupportedException
148
     *
149
     * @return string the SQL statement for creating a new index.
150
     *
151
     * {@see http://www.postgresql.org/docs/8.2/static/sql-createindex.html}
152
     */
153 6
    public function createIndex(string $name, string $table, $columns, bool $unique = false): string
154
    {
155 6
        if ($unique === self::INDEX_UNIQUE || $unique === true) {
156 4
            $index = false;
157 4
            $unique = true;
158
        } else {
159 3
            $index = $unique;
160 3
            $unique = false;
161
        }
162
163 6
        return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ')
164 6
            . $this->db->quoteTableName($name) . ' ON '
0 ignored issues
show
Bug introduced by
The method quoteTableName() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

164
            . $this->db->/** @scrutinizer ignore-call */ quoteTableName($name) . ' ON '

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
165 6
            . $this->db->quoteTableName($table)
166 6
            . ($index !== false ? " USING $index" : '')
0 ignored issues
show
introduced by
The condition $index !== false is always false.
Loading history...
167 6
            . ' (' . $this->buildColumns($columns) . ')';
168
    }
169
170
    /**
171
     * Builds a SQL statement for dropping an index.
172
     *
173
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
174
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
175
     *
176
     * @throws Exception
177
     * @throws InvalidConfigException
178
     * @throws NotSupportedException
179
     *
180
     * @return string the SQL statement for dropping an index.
181
     */
182 3
    public function dropIndex(string $name, string $table): string
183
    {
184 3
        if (strpos($table, '.') !== false && strpos($name, '.') === false) {
185 1
            if (strpos($table, '{{') !== false) {
186 1
                $table = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $table);
187 1
                [$schema, $table] = explode('.', $table);
188 1
                if (strpos($schema, '%') === false) {
189 1
                    $name = $schema . '.' . $name;
190
                } else {
191 1
                    $name = '{{' . $schema . '.' . $name . '}}';
192
                }
193
            } else {
194
                [$schema] = explode('.', $table);
195
                $name = $schema . '.' . $name;
196
            }
197
        }
198
199 3
        return 'DROP INDEX ' . $this->db->quoteTableName($name);
200
    }
201
202
    /**
203
     * Builds a SQL statement for renaming a DB table.
204
     *
205
     * @param string $oldName the table to be renamed. The name will be properly quoted by the method.
206
     * @param string $newName the new table name. The name will be properly quoted by the method.
207
     *
208
     * @throws Exception
209
     * @throws InvalidConfigException
210
     * @throws NotSupportedException
211
     *
212
     * @return string the SQL statement for renaming a DB table.
213
     */
214 2
    public function renameTable(string $oldName, string $newName): string
215
    {
216 2
        return 'ALTER TABLE ' . $this->db->quoteTableName($oldName) . ' RENAME TO '
217 2
            . $this->db->quoteTableName($newName);
218
    }
219
220
    /**
221
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
222
     *
223
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
224
     * or 1.
225
     *
226
     * @param string $tableName the name of the table whose primary key sequence will be reset.
227
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, the next new
228
     * row's primary key will have a value 1.
229
     *
230
     * @throws Exception
231
     * @throws InvalidArgumentException if the table does not exist or there is no sequence associated with the table.
232
     * @throws InvalidConfigException
233
     * @throws NotSupportedException
234
     *
235
     * @return string the SQL statement for resetting sequence.
236
     */
237 2
    public function resetSequence(string $tableName, $value = null): string
238
    {
239 2
        $table = $this->db->getTableSchema($tableName);
240 2
        if ($table !== null && $table->getSequenceName() !== null) {
241
            /** c.f. http://www.postgresql.org/docs/8.1/static/functions-sequence.html */
242 2
            $sequence = $this->db->quoteTableName($table->getSequenceName());
243 2
            $tableName = $this->db->quoteTableName($tableName);
244 2
            if ($value === null) {
245 2
                $pk = $table->getPrimaryKey();
246 2
                $key = $this->db->quoteColumnName(reset($pk));
247 2
                $value = "(SELECT COALESCE(MAX({$key}),0) FROM {$tableName})+1";
248
            } else {
249 2
                $value = (int) $value;
250
            }
251
252 2
            return "SELECT SETVAL('$sequence',$value,false)";
253
        }
254
255
        if ($table === null) {
256
            throw new InvalidArgumentException("Table not found: $tableName");
257
        }
258
259
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.");
260
    }
261
262
    /**
263
     * Builds a SQL statement for enabling or disabling integrity check.
264
     *
265
     * @param string $schema the schema of the tables.
266
     * @param string $table the table name.
267
     * @param bool $check whether to turn on or off the integrity check.
268
     *
269
     * @throws Exception
270
     * @throws InvalidConfigException
271
     * @throws NotSupportedException
272
     *
273
     * @return string the SQL statement for checking integrity.
274
     */
275
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
276
    {
277
        $enable = $check ? 'ENABLE' : 'DISABLE';
278
        $schema = $schema ?: $this->db->getSchema()->getDefaultSchema();
279
        $tableNames = $table ? [$table] : $this->db->getSchema()->getTableNames($schema);
0 ignored issues
show
Bug introduced by
It seems like $schema can also be of type null; however, parameter $schema of Yiisoft\Db\Schema\Schema::getTableNames() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

279
        $tableNames = $table ? [$table] : $this->db->getSchema()->getTableNames(/** @scrutinizer ignore-type */ $schema);
Loading history...
280
        $viewNames = $this->db->getSchema()->getViewNames($schema);
281
        $tableNames = array_diff($tableNames, $viewNames);
282
        $command = '';
283
284
        foreach ($tableNames as $tableName) {
285
            $tableName = $this->db->quoteTableName("{$schema}.{$tableName}");
286
            $command .= "ALTER TABLE $tableName $enable TRIGGER ALL; ";
287
        }
288
289
        /** enable to have ability to alter several tables */
290
        $this->db->getMasterPdo()->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
291
292
        return $command;
293
    }
294
295
    /**
296
     * Builds a SQL statement for truncating a DB table.
297
     *
298
     * Explicitly restarts identity for PGSQL to be consistent with other databases which all do this by default.
299
     *
300
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
301
     *
302
     * @throws Exception
303
     * @throws InvalidConfigException
304
     * @throws NotSupportedException
305
     *
306
     * @return string the SQL statement for truncating a DB table.
307
     */
308 1
    public function truncateTable(string $table): string
309
    {
310 1
        return 'TRUNCATE TABLE ' . $this->db->quoteTableName($table) . ' RESTART IDENTITY';
311
    }
312
313
    /**
314
     * Builds a SQL statement for changing the definition of a column.
315
     *
316
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the
317
     * method.
318
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
319
     * @param string $type the new column type. The {@see getColumnType()} method will be invoked to convert abstract
320
     * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept in the
321
     * generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become
322
     * 'varchar(255) not null'. You can also use PostgreSQL-specific syntax such as `SET NOT NULL`.
323
     *
324
     * @throws Exception
325
     * @throws InvalidConfigException
326
     * @throws NotSupportedException
327
     *
328
     * @return string the SQL statement for changing the definition of a column.
329
     */
330 2
    public function alterColumn(string $table, string $column, $type): string
331
    {
332 2
        $columnName = $this->db->quoteColumnName($column);
333 2
        $tableName = $this->db->quoteTableName($table);
334
335
        /**
336
         * {@see https://github.com/yiisoft/yii2/issues/4492}
337
         * {@see http://www.postgresql.org/docs/9.1/static/sql-altertable.html}
338
         */
339 2
        if (preg_match('/^(DROP|SET|RESET)\s+/i', (string) $type)) {
340 1
            return "ALTER TABLE {$tableName} ALTER COLUMN {$columnName} {$type}";
341
        }
342
343 2
        $type = 'TYPE ' . $this->getColumnType($type);
344 2
        $multiAlterStatement = [];
345 2
        $constraintPrefix = preg_replace('/[^a-z0-9_]/i', '', $table . '_' . $column);
346
347 2
        if (preg_match('/\s+DEFAULT\s+(["\']?\w*["\']?)/i', $type, $matches)) {
348 1
            $type = preg_replace('/\s+DEFAULT\s+(["\']?\w*["\']?)/i', '', $type);
349 1
            $multiAlterStatement[] = "ALTER COLUMN {$columnName} SET DEFAULT {$matches[1]}";
350
        } else {
351
            /** safe to drop default even if there was none in the first place */
352 2
            $multiAlterStatement[] = "ALTER COLUMN {$columnName} DROP DEFAULT";
353
        }
354
355 2
        $type = preg_replace('/\s+NOT\s+NULL/i', '', $type, -1, $count);
356
357 2
        if ($count) {
358 1
            $multiAlterStatement[] = "ALTER COLUMN {$columnName} SET NOT NULL";
359
        } else {
360
            /** remove additional null if any */
361 2
            $type = preg_replace('/\s+NULL/i', '', $type);
362
            /** safe to drop not null even if there was none in the first place */
363 2
            $multiAlterStatement[] = "ALTER COLUMN {$columnName} DROP NOT NULL";
364
        }
365
366 2
        if (preg_match('/\s+CHECK\s+\((.+)\)/i', $type, $matches)) {
367 1
            $type = preg_replace('/\s+CHECK\s+\((.+)\)/i', '', $type);
368 1
            $multiAlterStatement[] = "ADD CONSTRAINT {$constraintPrefix}_check CHECK ({$matches[1]})";
369
        }
370
371 2
        $type = preg_replace('/\s+UNIQUE/i', '', $type, -1, $count);
372
373 2
        if ($count) {
374 1
            $multiAlterStatement[] = "ADD UNIQUE ({$columnName})";
375
        }
376
377
        /** add what's left at the beginning */
378 2
        array_unshift($multiAlterStatement, "ALTER COLUMN {$columnName} {$type}");
379
380 2
        return 'ALTER TABLE ' . $tableName . ' ' . implode(', ', $multiAlterStatement);
381
    }
382
383
    /**
384
     * Creates an INSERT SQL statement.
385
     *
386
     * For example,.
387
     *
388
     * ```php
389
     * $sql = $queryBuilder->insert('user', [
390
     *     'name' => 'Sam',
391
     *     'age' => 30,
392
     * ], $params);
393
     * ```
394
     *
395
     * The method will properly escape the table and column names.
396
     *
397
     * @param string $table the table that new rows will be inserted into.
398
     * @param array|Query $columns the column data (name => value) to be inserted into the table or instance of
399
     * {@see \Yiisoft\Db\Query\Query|Query} to perform INSERT INTO ... SELECT SQL statement. Passing of
400
     * {@see \Yiisoft\Db\Query\Query|Query}.
401
     * @param array $params the binding parameters that will be generated by this method. They should be bound to the
402
     * DB command later.
403
     *
404
     * @throws Exception
405
     * @throws InvalidArgumentException
406
     * @throws InvalidConfigException
407
     * @throws NotSupportedException
408
     *
409
     * @return string the INSERT SQL
410
     */
411 44
    public function insert(string $table, $columns, array &$params = []): string
412
    {
413 44
        return parent::insert($table, $this->normalizeTableRowData($table, $columns), $params);
414
    }
415
416
    /**
417
     * Creates an SQL statement to insert rows into a database table if they do not already exist (matching unique
418
     * constraints), or update them if they do.
419
     *
420
     * For example,
421
     *
422
     * ```php
423
     * $sql = $queryBuilder->upsert('pages', [
424
     *     'name' => 'Front page',
425
     *     'url' => 'http://example.com/', // url is unique
426
     *     'visits' => 0,
427
     * ], [
428
     *     'visits' => new \Yiisoft\Db\Expression('visits + 1'),
429
     * ], $params);
430
     * ```
431
     *
432
     * The method will properly escape the table and column names.
433
     *
434
     * @param string $table the table that new rows will be inserted into/updated in.
435
     * @param array|Query $insertColumns the column data (name => value) to be inserted into the table or instance of
436
     * {@see Query} to perform `INSERT INTO ... SELECT` SQL statement.
437
     * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist.
438
     * If `true` is passed, the column data will be updated to match the insert column data.
439
     * If `false` is passed, no update will be performed if the column data already exists.
440
     * @param array $params the binding parameters that will be generated by this method.
441
     * They should be bound to the DB command later.
442
     *
443
     * @throws Exception
444
     * @throws InvalidConfigException
445
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
446
     *
447
     * @return string the resulting SQL.
448
     *
449
     * {@see https://www.postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT}
450
     * {@see https://stackoverflow.com/questions/1109061/insert-on-duplicate-update-in-postgresql/8702291#8702291}
451
     */
452 18
    public function upsert(string $table, $insertColumns, $updateColumns, array &$params = []): string
453
    {
454 18
        $insertColumns = $this->normalizeTableRowData($table, $insertColumns);
455
456 18
        if (!is_bool($updateColumns)) {
457 7
            $updateColumns = $this->normalizeTableRowData($table, $updateColumns);
458
        }
459 18
        if (version_compare($this->db->getServerVersion(), '9.5', '<')) {
460
            return $this->oldUpsert($table, $insertColumns, $updateColumns, $params);
461
        }
462
463 18
        return $this->newUpsert($table, $insertColumns, $updateColumns, $params);
464
    }
465
466
    /**
467
     * {@see upsert()} implementation for PostgreSQL 9.5 or higher.
468
     *
469
     * @param string $table
470
     * @param array|Query $insertColumns
471
     * @param array|bool $updateColumns
472
     * @param array $params
473
     *
474
     * @throws Exception
475
     * @throws InvalidArgumentException
476
     * @throws InvalidConfigException
477
     * @throws NotSupportedException
478
     *
479
     * @return string
480
     */
481 18
    private function newUpsert(string $table, $insertColumns, $updateColumns, array &$params = []): string
482
    {
483 18
        $insertSql = $this->insert($table, $insertColumns, $params);
484 18
        [$uniqueNames, , $updateNames] = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns);
485
486 18
        if (empty($uniqueNames)) {
487 3
            return $insertSql;
488
        }
489
490 15
        if ($updateNames === []) {
491
            /** there are no columns to update */
492
            $updateColumns = false;
493
        }
494
495 15
        if ($updateColumns === false) {
496 5
            return "$insertSql ON CONFLICT DO NOTHING";
497
        }
498
499 10
        if ($updateColumns === true) {
500 4
            $updateColumns = [];
501 4
            foreach ($updateNames as $name) {
502 4
                $updateColumns[$name] = new Expression('EXCLUDED.' . $this->db->quoteColumnName($name));
503
            }
504
        }
505
506 10
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
507
508 10
        return $insertSql . ' ON CONFLICT (' . implode(', ', $uniqueNames) . ') DO UPDATE SET '
509 10
            . implode(', ', $updates);
510
    }
511
512
    /**
513
     * {@see upsert()} implementation for PostgreSQL older than 9.5.
514
     *
515
     * @param string $table
516
     * @param array|Query $insertColumns
517
     * @param array|bool $updateColumns
518
     * @param array $params
519
     *
520
     * @throws Exception
521
     * @throws InvalidArgumentException
522
     * @throws InvalidConfigException
523
     * @throws NotSupportedException
524
     *
525
     * @return string
526
     */
527
    private function oldUpsert(string $table, $insertColumns, $updateColumns, array &$params = []): string
528
    {
529
        /** @var Constraint[] $constraints */
530
        [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns(
531
            $table,
532
            $insertColumns,
533
            $updateColumns,
534
            $constraints
535
        );
536
537
        if (empty($uniqueNames)) {
538
            return $this->insert($table, $insertColumns, $params);
539
        }
540
541
        if ($updateNames === []) {
542
            /** there are no columns to update */
543
            $updateColumns = false;
544
        }
545
546
        /** @var Schema $schema */
547
        $schema = $this->db->getSchema();
548
549
        if (!$insertColumns instanceof Query) {
550
            $tableSchema = $schema->getTableSchema($table);
551
            $columnSchemas = $tableSchema !== null ? $tableSchema->getColumns() : [];
552
            foreach ($insertColumns as $name => $value) {
553
                /**
554
                 * NULLs and numeric values must be type hinted in order to be used in SET assigments NVM, let's cast
555
                 * them all
556
                 */
557
                if (isset($columnSchemas[$name])) {
558
                    $phName = self::PARAM_PREFIX . count($params);
559
                    $params[$phName] = $value;
560
                    $insertColumns[$name] = new Expression("CAST($phName AS {$columnSchemas[$name]->getDbType()})");
561
                }
562
            }
563
        }
564
565
        [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);
566
        $updateCondition = ['or'];
567
        $insertCondition = ['or'];
568
        $quotedTableName = $schema->quoteTableName($table);
569
570
        foreach ($constraints as $constraint) {
571
            $constraintUpdateCondition = ['and'];
572
            $constraintInsertCondition = ['and'];
573
            foreach ($constraint->getColumnNames() as $name) {
574
                $quotedName = $schema->quoteColumnName($name);
575
                $constraintUpdateCondition[] = "$quotedTableName.$quotedName=\"EXCLUDED\".$quotedName";
576
                $constraintInsertCondition[] = "\"upsert\".$quotedName=\"EXCLUDED\".$quotedName";
577
            }
578
            $updateCondition[] = $constraintUpdateCondition;
579
            $insertCondition[] = $constraintInsertCondition;
580
        }
581
582
        $withSql = 'WITH "EXCLUDED" (' . implode(', ', $insertNames) . ') AS ('
583
            . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ')';
584
585
        if ($updateColumns === false) {
586
            $selectSubQuery = (new Query())
587
                ->select(new Expression('1'))
588
                ->from($table)
589
                ->where($updateCondition);
590
            $insertSelectSubQuery = (new Query())
591
                ->select($insertNames)
592
                ->from('EXCLUDED')
593
                ->where(['not exists', $selectSubQuery]);
594
            $insertSql = $this->insert($table, $insertSelectSubQuery, $params);
595
596
            return "$withSql $insertSql";
597
        }
598
599
        if ($updateColumns === true) {
600
            $updateColumns = [];
601
            foreach ($updateNames as $name) {
602
                $quotedName = $this->db->quoteColumnName($name);
603
                if (\strrpos($quotedName, '.') === false) {
604
                    $quotedName = '"EXCLUDED".' . $quotedName;
605
                }
606
                $updateColumns[$name] = new Expression($quotedName);
607
            }
608
        }
609
610
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
611
612
        $updateSql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $updates)
613
            . ' FROM "EXCLUDED" ' . $this->buildWhere($updateCondition, $params)
614
            . ' RETURNING ' . $this->db->quoteTableName($table) . '.*';
615
616
        $selectUpsertSubQuery = (new Query())
617
            ->select(new Expression('1'))
618
            ->from('upsert')
619
            ->where($insertCondition);
620
621
        $insertSelectSubQuery = (new Query())
622
            ->select($insertNames)
623
            ->from('EXCLUDED')
624
            ->where(['not exists', $selectUpsertSubQuery]);
625
626
        $insertSql = $this->insert($table, $insertSelectSubQuery, $params);
627
628
        return "$withSql, \"upsert\" AS ($updateSql) $insertSql";
629
    }
630
631
    /**
632
     * Creates an UPDATE SQL statement.
633
     *
634
     * For example,
635
     *
636
     * ```php
637
     * $params = [];
638
     * $sql = $queryBuilder->update('user', ['status' => 1], 'age > 30', $params);
639
     * ```
640
     *
641
     * The method will properly escape the table and column names.
642
     *
643
     * @param string $table the table to be updated.
644
     * @param array $columns the column data (name => value) to be updated.
645
     * @param array|string $condition the condition that will be put in the WHERE part. Please refer to
646
     * {@see Query::where()} on how to specify condition.
647
     * @param array $params the binding parameters that will be modified by this method so that they can be bound to the
648
     * DB command later.
649
     *
650
     * @throws Exception
651
     * @throws InvalidArgumentException
652
     * @throws InvalidConfigException
653
     * @throws NotSupportedException
654
     *
655
     * @return string the UPDATE SQL.
656
     */
657 4
    public function update(string $table, array $columns, $condition, array &$params = []): string
658
    {
659 4
        return parent::update($table, $this->normalizeTableRowData($table, $columns), $condition, $params);
660
    }
661
662
    /**
663
     * Normalizes data to be saved into the table, performing extra preparations and type converting, if necessary.
664
     *
665
     * @param string $table the table that data will be saved into.
666
     * @param array|Query $columns the column data (name => value) to be saved into the table or instance of
667
     * {@see \Yiisoft\Db\Query\Query} to perform INSERT INTO ... SELECT SQL statement. Passing of
668
     * {@see \Yiisoft\Db\Query\Query}.
669
     *
670
     * @throws Exception
671
     * @throws InvalidConfigException
672
     * @throws NotSupportedException
673
     *
674
     * @return array|object normalized columns
675
     */
676 46
    private function normalizeTableRowData(string $table, $columns)
677
    {
678 46
        if ($columns instanceof Query) {
679 14
            return $columns;
680
        }
681
682 38
        if (($tableSchema = $this->db->getSchema()->getTableSchema($table)) !== null) {
683 38
            $columnSchemas = $tableSchema->getColumns();
684 38
            foreach ($columns as $name => $value) {
685
                if (
686 38
                    isset($columnSchemas[$name]) &&
687 38
                    $columnSchemas[$name]->getType() === Schema::TYPE_BINARY &&
688 38
                    is_string($value)
689
                ) {
690
                    /** explicitly setup PDO param type for binary column */
691 1
                    $columns[$name] = new PdoValue($value, PDO::PARAM_LOB);
692
                }
693
            }
694
        }
695
696 38
        return $columns;
697
    }
698
699
    /**
700
     * Generates a batch INSERT SQL statement.
701
     *
702
     * For example,
703
     *
704
     * ```php
705
     * $sql = $queryBuilder->batchInsert('user', ['name', 'age'], [
706
     *     ['Tom', 30],
707
     *     ['Jane', 20],
708
     *     ['Linda', 25],
709
     * ]);
710
     * ```
711
     *
712
     * Note that the values in each row must match the corresponding column names.
713
     *
714
     * The method will properly escape the column names, and quote the values to be inserted.
715
     *
716
     * @param string $table the table that new rows will be inserted into.
717
     * @param array $columns the column names.
718
     * @param array|Generator $rows the rows to be batch inserted into the table.
0 ignored issues
show
Bug introduced by
The type Yiisoft\Db\Pgsql\Query\Generator was not found. Did you mean Generator? If so, make sure to prefix the type with \.
Loading history...
719
     * @param array $params the binding parameters. This parameter exists.
720
     *
721
     * @throws Exception
722
     * @throws InvalidArgumentException
723
     * @throws InvalidConfigException
724
     * @throws NotSupportedException
725
     *
726
     * @return string the batch INSERT SQL statement.
727
     */
728 20
    public function batchInsert(string $table, array $columns, $rows, array &$params = []): string
729
    {
730 20
        if (empty($rows)) {
731 2
            return '';
732
        }
733
734 19
        $schema = $this->db->getSchema();
735 19
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
736 19
            $columnSchemas = $tableSchema->getColumns();
737
        } else {
738
            $columnSchemas = [];
739
        }
740
741 19
        $values = [];
742 19
        foreach ($rows as $row) {
743 18
            $vs = [];
744 18
            foreach ($row as $i => $value) {
745 18
                if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
746 15
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
747
                }
748 18
                if (is_string($value)) {
749 8
                    $value = $schema->quoteValue($value);
750 13
                } elseif (is_float($value)) {
751
                    // ensure type cast always has . as decimal separator in all locales
752 1
                    $value = StringHelper::floatToString($value);
753 13
                } elseif ($value === true) {
754 3
                    $value = 'TRUE';
755 13
                } elseif ($value === false) {
756 5
                    $value = 'FALSE';
757 11
                } elseif ($value === null) {
758 4
                    $value = 'NULL';
759 8
                } elseif ($value instanceof ExpressionInterface) {
760 6
                    $value = $this->buildExpression($value, $params);
761
                }
762 18
                $vs[] = $value;
763
            }
764 18
            $values[] = '(' . implode(', ', $vs) . ')';
765
        }
766
767 19
        if (empty($values)) {
768 1
            return '';
769
        }
770
771 18
        foreach ($columns as $i => $name) {
772 17
            $columns[$i] = $schema->quoteColumnName($name);
773
        }
774
775 18
        return 'INSERT INTO ' . $schema->quoteTableName($table)
776 18
            . ' (' . implode(', ', $columns) . ') VALUES ' . implode(', ', $values);
777
    }
778
}
779