Passed
Push — master ( 14edf8...640dd0 )
by Alexander
08:24
created

QueryBuilder::alterColumn()   B

Complexity

Conditions 6
Paths 17

Size

Total Lines 50
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 26
c 0
b 0
f 0
nc 17
nop 3
dl 0
loc 50
ccs 25
cts 25
cp 1
crap 6
rs 8.8817
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