Passed
Push — master ( ce698f...fcbf0c )
by Wilmer
10:25 queued 08:39
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\InvalidArgumentException;
9
use Yiisoft\Db\Expression\ArrayExpression;
10
use Yiisoft\Db\Expression\JsonExpression;
11
use Yiisoft\Db\Expression\Expression;
12
use Yiisoft\Db\Expression\ExpressionInterface;
13
use Yiisoft\Db\Pgsql\Expression\ArrayExpressionBuilder;
14
use Yiisoft\Db\Pgsql\Expression\JsonExpressionBuilder;
15
use Yiisoft\Db\Pgsql\Schema\Schema;
16
use Yiisoft\Db\Pdo\PdoValue;
17
use Yiisoft\Db\Query\Query;
18
use Yiisoft\Db\Query\QueryBuilder as AbstractQueryBuilder;
19
use Yiisoft\Db\Query\Conditions\LikeCondition;
20
use Yiisoft\Strings\StringHelper;
21
22
/**
23
 * QueryBuilder is the query builder for PostgreSQL databases.
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
     * {@inheritdoc}
82
     */
83 275
    protected function defaultConditionClasses(): array
84
    {
85 275
        return \array_merge(parent::defaultConditionClasses(), [
86 275
            'ILIKE' => LikeCondition::class,
87
            'NOT ILIKE' => LikeCondition::class,
88
            'OR ILIKE' => LikeCondition::class,
89
            'OR NOT ILIKE' => LikeCondition::class,
90
        ]);
91
    }
92
93
    /**
94
     * {@inheritdoc}
95
     */
96 275
    protected function defaultExpressionBuilders(): array
97
    {
98 275
        return \array_merge(parent::defaultExpressionBuilders(), [
99 275
            ArrayExpression::class => ArrayExpressionBuilder::class,
100
            JsonExpression::class => JsonExpressionBuilder::class,
101
        ]);
102
    }
103
104
    /**
105
     * Builds a SQL statement for creating a new index.
106
     * @param string $name the name of the index. The name will be properly quoted by the method.
107
     * @param string $table the table that the new index will be created for. The table name will be properly quoted by
108
     * the method.
109
     * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns,
110
     * separate them with commas or use an array to represent them. Each column name will be properly quoted
111
     * by the method, unless a parenthesis is found in the name.
112
     * @param bool|string $unique whether to make this a UNIQUE index constraint. You can pass `true` or
113
     * {@see INDEX_UNIQUE} to create
114
     * a unique index, `false` to make a non-unique index using the default index type, or one of the following
115
     * constants to specify the index method to use: {@see INDEX_B_TREE}, {@see INDEX_HASH}, {@see INDEX_GIST},
116
     * {@see INDEX_GIN}.
117
     *
118
     * @return string the SQL statement for creating a new index.
119
     *
120
     * {@see http://www.postgresql.org/docs/8.2/static/sql-createindex.html}
121
     */
122 6
    public function createIndex(string $name, string $table, $columns, bool $unique = false): string
123
    {
124 6
        if ($unique === self::INDEX_UNIQUE || $unique === true) {
125 4
            $index = false;
126 4
            $unique = true;
127
        } else {
128 3
            $index = $unique;
129 3
            $unique = false;
130
        }
131
132 6
        return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ') .
133 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

133
        $this->db->/** @scrutinizer ignore-call */ 
134
                   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...
134 6
        $this->db->quoteTableName($table) .
135 6
        ($index !== false ? " USING $index" : '') .
0 ignored issues
show
introduced by
The condition $index !== false is always false.
Loading history...
136 6
        ' (' . $this->buildColumns($columns) . ')';
137
    }
138
139
    /**
140
     * Builds a SQL statement for dropping an index.
141
     *
142
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
143
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
144
     *
145
     * @return string the SQL statement for dropping an index.
146
     */
147 3
    public function dropIndex(string $name, string $table): string
148
    {
149 3
        if (\strpos($table, '.') !== false && \strpos($name, '.') === false) {
150 1
            if (\strpos($table, '{{') !== false) {
151 1
                $table = \preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $table);
152 1
                [$schema, $table] = \explode('.', $table);
153 1
                if (\strpos($schema, '%') === false) {
154 1
                    $name = $schema . '.' . $name;
155
                } else {
156 1
                    $name = '{{' . $schema . '.' . $name . '}}';
157
                }
158
            } else {
159
                [$schema] = \explode('.', $table);
160
                $name = $schema . '.' . $name;
161
            }
162
        }
163
164 3
        return 'DROP INDEX ' . $this->db->quoteTableName($name);
165
    }
166
167
    /**
168
     * Builds a SQL statement for renaming a DB table.
169
     *
170
     * @param string $oldName the table to be renamed. The name will be properly quoted by the method.
171
     * @param string $newName the new table name. The name will be properly quoted by the method.
172
     *
173
     * @return string the SQL statement for renaming a DB table.
174
     */
175 2
    public function renameTable(string $oldName, string $newName): string
176
    {
177 2
        return 'ALTER TABLE ' . $this->db->quoteTableName($oldName) . ' RENAME TO '
178 2
            . $this->db->quoteTableName($newName);
179
    }
180
181
    /**
182
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
183
     *
184
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified
185
     * value or 1.
186
     *
187
     * @param string $tableName the name of the table whose primary key sequence will be reset
188
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set,
189
     * the next new row's primary key will have a value 1.
190
     *
191
     * @throws InvalidArgumentException if the table does not exist or there is no sequence associated with the table.
192
     *
193
     * @return string the SQL statement for resetting sequence
194
     */
195 1
    public function resetSequence(string $tableName, $value = null): string
196
    {
197 1
        $table = $this->db->getTableSchema($tableName);
198 1
        if ($table !== null && $table->getSequenceName() !== null) {
199
            /** c.f. http://www.postgresql.org/docs/8.1/static/functions-sequence.html */
200 1
            $sequence = $this->db->quoteTableName($table->getSequenceName());
201 1
            $tableName = $this->db->quoteTableName($tableName);
202 1
            if ($value === null) {
203 1
                $pk = $table->getPrimaryKey();
204 1
                $key = $this->db->quoteColumnName(reset($pk));
205 1
                $value = "(SELECT COALESCE(MAX({$key}),0) FROM {$tableName})+1";
206
            } else {
207 1
                $value = (int) $value;
208
            }
209
210 1
            return "SELECT SETVAL('$sequence',$value,false)";
211
        }
212
213
        if ($table === null) {
214
            throw new InvalidArgumentException("Table not found: $tableName");
215
        }
216
217
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.");
218
    }
219
220
    /**
221
     * Builds a SQL statement for enabling or disabling integrity check.
222
     *
223
     * @param bool $check whether to turn on or off the integrity check.
224
     * @param string $schema the schema of the tables.
225
     * @param string $table the table name.
226
     *
227
     * @return string the SQL statement for checking integrity
228
     */
229
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
230
    {
231
        $enable = $check ? 'ENABLE' : 'DISABLE';
232
        $schema = $schema ?: $this->db->getSchema()->defaultSchema;
0 ignored issues
show
Bug introduced by
The property defaultSchema is declared protected in Yiisoft\Db\Schema\Schema and cannot be accessed from this context.
Loading history...
233
        $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

233
        $tableNames = $table ? [$table] : $this->db->getSchema()->getTableNames(/** @scrutinizer ignore-type */ $schema);
Loading history...
234
        $viewNames = $this->db->getSchema()->getViewNames($schema);
235
        $tableNames = \array_diff($tableNames, $viewNames);
236
        $command = '';
237
238
        foreach ($tableNames as $tableName) {
239
            $tableName = $this->db->quoteTableName("{$schema}.{$tableName}");
240
            $command .= "ALTER TABLE $tableName $enable TRIGGER ALL; ";
241
        }
242
243
        // enable to have ability to alter several tables
244
        $this->db->getMasterPdo()->setAttribute(\PDO::ATTR_EMULATE_PREPARES, true);
245
246
        return $command;
247
    }
248
249
    /**
250
     * Builds a SQL statement for truncating a DB table.
251
     *
252
     * Explicitly restarts identity for PGSQL to be consistent with other databases which all do this by default.
253
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
254
     *
255
     * @return string the SQL statement for truncating a DB table.
256
     */
257 1
    public function truncateTable(string $table): string
258
    {
259 1
        return 'TRUNCATE TABLE ' . $this->db->quoteTableName($table) . ' RESTART IDENTITY';
260
    }
261
262
    /**
263
     * Builds a SQL statement for changing the definition of a column.
264
     *
265
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the
266
     * method.
267
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
268
     * @param string $type the new column type. The {@see getColumnType()} method will be invoked to convert abstract
269
     * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept
270
     * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null'
271
     * will become 'varchar(255) not null'. You can also use PostgreSQL-specific syntax such as `SET NOT NULL`.
272
     *
273
     * @return string the SQL statement for changing the definition of a column.
274
     */
275 2
    public function alterColumn(string $table, string $column, $type): string
276
    {
277 2
        $columnName = $this->db->quoteColumnName($column);
278 2
        $tableName = $this->db->quoteTableName($table);
279
280
        /**
281
         * https://github.com/yiisoft/yii2/issues/4492
282
         * http://www.postgresql.org/docs/9.1/static/sql-altertable.html
283
         */
284 2
        if (\preg_match('/^(DROP|SET|RESET)\s+/i', (string) $type)) {
285 1
            return "ALTER TABLE {$tableName} ALTER COLUMN {$columnName} {$type}";
286
        }
287
288 2
        $type = 'TYPE ' . $this->getColumnType($type);
289
290 2
        $multiAlterStatement = [];
291 2
        $constraintPrefix = \preg_replace('/[^a-z0-9_]/i', '', $table . '_' . $column);
292
293 2
        if (\preg_match('/\s+DEFAULT\s+(["\']?\w*["\']?)/i', $type, $matches)) {
294 1
            $type = \preg_replace('/\s+DEFAULT\s+(["\']?\w*["\']?)/i', '', $type);
295 1
            $multiAlterStatement[] = "ALTER COLUMN {$columnName} SET DEFAULT {$matches[1]}";
296
        } else {
297
            // safe to drop default even if there was none in the first place
298 2
            $multiAlterStatement[] = "ALTER COLUMN {$columnName} DROP DEFAULT";
299
        }
300
301 2
        $type = \preg_replace('/\s+NOT\s+NULL/i', '', $type, -1, $count);
302 2
        if ($count) {
303 1
            $multiAlterStatement[] = "ALTER COLUMN {$columnName} SET NOT NULL";
304
        } else {
305
            // remove additional null if any
306 2
            $type = \preg_replace('/\s+NULL/i', '', $type);
307
            // safe to drop not null even if there was none in the first place
308 2
            $multiAlterStatement[] = "ALTER COLUMN {$columnName} DROP NOT NULL";
309
        }
310
311 2
        if (\preg_match('/\s+CHECK\s+\((.+)\)/i', $type, $matches)) {
312 1
            $type = \preg_replace('/\s+CHECK\s+\((.+)\)/i', '', $type);
313 1
            $multiAlterStatement[] = "ADD CONSTRAINT {$constraintPrefix}_check CHECK ({$matches[1]})";
314
        }
315
316 2
        $type = \preg_replace('/\s+UNIQUE/i', '', $type, -1, $count);
317 2
        if ($count) {
318 1
            $multiAlterStatement[] = "ADD UNIQUE ({$columnName})";
319
        }
320
321
        // add what's left at the beginning
322 2
        \array_unshift($multiAlterStatement, "ALTER COLUMN {$columnName} {$type}");
323
324 2
        return 'ALTER TABLE ' . $tableName . ' ' . \implode(', ', $multiAlterStatement);
325
    }
326
327
    /**
328
     * {@inheritdoc}
329
     */
330 44
    public function insert(string $table, $columns, array &$params = []): string
331
    {
332 44
        return parent::insert($table, $this->normalizeTableRowData($table, $columns), $params);
333
    }
334
335
    /**
336
     * {@inheritdoc}
337
     *
338
     * {@see https://www.postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT}
339
     * {@see https://stackoverflow.com/questions/1109061/insert-on-duplicate-update-in-postgresql/8702291#8702291}
340
     */
341 18
    public function upsert(string $table, $insertColumns, $updateColumns, array &$params = []): string
342
    {
343 18
        $insertColumns = $this->normalizeTableRowData($table, $insertColumns);
344
345 18
        if (!\is_bool($updateColumns)) {
346 7
            $updateColumns = $this->normalizeTableRowData($table, $updateColumns);
347
        }
348 18
        if (\version_compare($this->db->getServerVersion(), '9.5', '<')) {
349
            return $this->oldUpsert($table, $insertColumns, $updateColumns, $params);
0 ignored issues
show
Bug introduced by
It seems like $updateColumns can also be of type object; however, parameter $updateColumns of Yiisoft\Db\Pgsql\Query\QueryBuilder::oldUpsert() does only seem to accept array|boolean, 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

349
            return $this->oldUpsert($table, $insertColumns, /** @scrutinizer ignore-type */ $updateColumns, $params);
Loading history...
350
        }
351
352 18
        return $this->newUpsert($table, $insertColumns, $updateColumns, $params);
0 ignored issues
show
Bug introduced by
It seems like $updateColumns can also be of type object; however, parameter $updateColumns of Yiisoft\Db\Pgsql\Query\QueryBuilder::newUpsert() does only seem to accept array|boolean, 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

352
        return $this->newUpsert($table, $insertColumns, /** @scrutinizer ignore-type */ $updateColumns, $params);
Loading history...
353
    }
354
355
    /**
356
     * {@see upsert()} implementation for PostgreSQL 9.5 or higher.
357
     *
358
     * @param string $table
359
     * @param array|Query $insertColumns
360
     * @param array|bool $updateColumns
361
     * @param array $params
362
     *
363
     * @return string
364
     */
365 18
    private function newUpsert($table, $insertColumns, $updateColumns, &$params): string
366
    {
367 18
        $insertSql = $this->insert($table, $insertColumns, $params);
368 18
        [$uniqueNames, , $updateNames] = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns);
369
370 18
        if (empty($uniqueNames)) {
371 3
            return $insertSql;
372
        }
373
374 15
        if ($updateNames === []) {
375
            // there are no columns to update
376
            $updateColumns = false;
377
        }
378
379 15
        if ($updateColumns === false) {
380 5
            return "$insertSql ON CONFLICT DO NOTHING";
381
        }
382
383 10
        if ($updateColumns === true) {
384 4
            $updateColumns = [];
385 4
            foreach ($updateNames as $name) {
386 4
                $updateColumns[$name] = new Expression('EXCLUDED.' . $this->db->quoteColumnName($name));
387
            }
388
        }
389
390 10
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
391
392 10
        return $insertSql . ' ON CONFLICT (' . implode(', ', $uniqueNames) . ') DO UPDATE SET '
393 10
            . \implode(', ', $updates);
394
    }
395
396
    /**
397
     * {@see upsert()} implementation for PostgreSQL older than 9.5.
398
     *
399
     * @param string $table
400
     * @param array|Query $insertColumns
401
     * @param array|bool $updateColumns
402
     * @param array $params
403
     *
404
     * @return string
405
     */
406
    private function oldUpsert($table, $insertColumns, $updateColumns, &$params)
407
    {
408
        /** @var Constraint[] $constraints */
409
        [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns(
410
            $table,
411
            $insertColumns,
412
            $updateColumns,
413
            $constraints
414
        );
415
416
        if (empty($uniqueNames)) {
417
            return $this->insert($table, $insertColumns, $params);
418
        }
419
420
        if ($updateNames === []) {
421
            /** there are no columns to update */
422
            $updateColumns = false;
423
        }
424
425
        /** @var Schema $schema */
426
        $schema = $this->db->getSchema();
427
428
        if (!$insertColumns instanceof Query) {
429
            $tableSchema = $schema->getTableSchema($table);
430
            $columnSchemas = $tableSchema !== null ? $tableSchema->columns : [];
0 ignored issues
show
Bug introduced by
The property columns is declared private in Yiisoft\Db\Schema\TableSchema and cannot be accessed from this context.
Loading history...
431
            foreach ($insertColumns as $name => $value) {
432
                // NULLs and numeric values must be type hinted in order to be used in SET assigments
433
                // NVM, let's cast them all
434
                if (isset($columnSchemas[$name])) {
435
                    $phName = self::PARAM_PREFIX . \count($params);
436
                    $params[$phName] = $value;
437
                    $insertColumns[$name] = new Expression("CAST($phName AS {$columnSchemas[$name]->dbType})");
438
                }
439
            }
440
        }
441
442
        [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);
443
        $updateCondition = ['or'];
444
        $insertCondition = ['or'];
445
        $quotedTableName = $schema->quoteTableName($table);
446
447
        foreach ($constraints as $constraint) {
448
            $constraintUpdateCondition = ['and'];
449
            $constraintInsertCondition = ['and'];
450
            foreach ($constraint->getColumnNames() as $name) {
451
                $quotedName = $schema->quoteColumnName($name);
452
                $constraintUpdateCondition[] = "$quotedTableName.$quotedName=\"EXCLUDED\".$quotedName";
453
                $constraintInsertCondition[] = "\"upsert\".$quotedName=\"EXCLUDED\".$quotedName";
454
            }
455
            $updateCondition[] = $constraintUpdateCondition;
456
            $insertCondition[] = $constraintInsertCondition;
457
        }
458
459
        $withSql = 'WITH "EXCLUDED" (' . \implode(', ', $insertNames) . ') AS ('
460
            . (!empty($placeholders) ? 'VALUES (' . \implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ')';
461
462
        if ($updateColumns === false) {
463
            $selectSubQuery = (new Query())
0 ignored issues
show
Bug introduced by
The call to Yiisoft\Db\Query\Query::__construct() has too few arguments starting with db. ( Ignorable by Annotation )

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

463
            $selectSubQuery = (/** @scrutinizer ignore-call */ new Query())

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
464
                ->select(new Expression('1'))
465
                ->from($table)
466
                ->where($updateCondition);
467
            $insertSelectSubQuery = (new Query())
468
                ->select($insertNames)
469
                ->from('EXCLUDED')
470
                ->where(['not exists', $selectSubQuery]);
471
            $insertSql = $this->insert($table, $insertSelectSubQuery, $params);
472
473
            return "$withSql $insertSql";
474
        }
475
476
        if ($updateColumns === true) {
477
            $updateColumns = [];
478
            foreach ($updateNames as $name) {
479
                $quotedName = $this->db->quoteColumnName($name);
480
                if (strrpos($quotedName, '.') === false) {
481
                    $quotedName = '"EXCLUDED".' . $quotedName;
482
                }
483
                $updateColumns[$name] = new Expression($quotedName);
484
            }
485
        }
486
487
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
488
489
        $updateSql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $updates)
490
            . ' FROM "EXCLUDED" ' . $this->buildWhere($updateCondition, $params)
491
            . ' RETURNING ' . $this->db->quoteTableName($table) . '.*';
492
493
        $selectUpsertSubQuery = (new Query())
494
            ->select(new Expression('1'))
495
            ->from('upsert')
496
            ->where($insertCondition);
497
498
        $insertSelectSubQuery = (new Query())
499
            ->select($insertNames)
500
            ->from('EXCLUDED')
501
            ->where(['not exists', $selectUpsertSubQuery]);
502
503
        $insertSql = $this->insert($table, $insertSelectSubQuery, $params);
504
505
        return "$withSql, \"upsert\" AS ($updateSql) $insertSql";
506
    }
507
508
    /**
509
     * {@inheritdoc}
510
     */
511 4
    public function update(string $table, array $columns, $condition, array &$params = []): string
512
    {
513 4
        return parent::update($table, $this->normalizeTableRowData($table, $columns), $condition, $params);
514
    }
515
516
    /**
517
     * Normalizes data to be saved into the table, performing extra preparations and type converting, if necessary.
518
     *
519
     * @param string $table the table that data will be saved into.
520
     * @param array|Query $columns the column data (name => value) to be saved into the table or instance
521
     * of {@see Querys\Query|Query} to perform INSERT INTO ... SELECT SQL statement.
522
     * Passing of {@see Querys|Query}.
523
     *
524
     * @return array|object normalized columns
525
     */
526 46
    private function normalizeTableRowData($table, $columns)
527
    {
528 46
        if ($columns instanceof Query) {
529 14
            return $columns;
530
        }
531
532 38
        if (($tableSchema = $this->db->getSchema()->getTableSchema($table)) !== null) {
533 38
            $columnSchemas = $tableSchema->getColumns();
534 38
            foreach ($columns as $name => $value) {
535 38
                if (isset($columnSchemas[$name]) && $columnSchemas[$name]->getType() === Schema::TYPE_BINARY && \is_string($value)) {
536
                    /** explicitly setup PDO param type for binary column */
537 1
                    $columns[$name] = new PdoValue($value, \PDO::PARAM_LOB);
538
                }
539
            }
540
        }
541
542 38
        return $columns;
543
    }
544
545
    /**
546
     * {@inheritdoc}
547
     */
548 20
    public function batchInsert(string $table, array $columns, $rows, array &$params = []): string
549
    {
550 20
        if (empty($rows)) {
551 2
            return '';
552
        }
553
554 19
        $schema = $this->db->getSchema();
555 19
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
556 19
            $columnSchemas = $tableSchema->getColumns();
557
        } else {
558
            $columnSchemas = [];
559
        }
560
561 19
        $values = [];
562 19
        foreach ($rows as $row) {
563 18
            $vs = [];
564 18
            foreach ($row as $i => $value) {
565 18
                if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
566 15
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
567
                }
568 18
                if (\is_string($value)) {
569 8
                    $value = $schema->quoteValue($value);
570 13
                } elseif (\is_float($value)) {
571
                    // ensure type cast always has . as decimal separator in all locales
572 1
                    $value = StringHelper::floatToString($value);
573 13
                } elseif ($value === true) {
574 3
                    $value = 'TRUE';
575 13
                } elseif ($value === false) {
576 5
                    $value = 'FALSE';
577 11
                } elseif ($value === null) {
578 4
                    $value = 'NULL';
579 8
                } elseif ($value instanceof ExpressionInterface) {
580 6
                    $value = $this->buildExpression($value, $params);
581
                }
582 18
                $vs[] = $value;
583
            }
584 18
            $values[] = '(' . \implode(', ', $vs) . ')';
585
        }
586
587 19
        if (empty($values)) {
588 1
            return '';
589
        }
590
591 18
        foreach ($columns as $i => $name) {
592 17
            $columns[$i] = $schema->quoteColumnName($name);
593
        }
594
595 18
        return 'INSERT INTO ' . $schema->quoteTableName($table)
596 18
        . ' (' . \implode(', ', $columns) . ') VALUES ' . \implode(', ', $values);
597
    }
598
}
599