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

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

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