Passed
Pull Request — master (#19)
by Wilmer
12:28
created

QueryBuilder::batchInsert()   C

Complexity

Conditions 14
Paths 97

Size

Total Lines 49
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
eloc 33
c 0
b 0
f 0
nc 97
nop 4
dl 0
loc 49
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
    protected function defaultConditionClasses(): array
84
    {
85
        return \array_merge(parent::defaultConditionClasses(), [
86
            '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
    protected function defaultExpressionBuilders(): array
97
    {
98
        return \array_merge(parent::defaultExpressionBuilders(), [
99
            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
    public function createIndex(string $name, string $table, $columns, bool $unique = false): string
123
    {
124
        if ($unique === self::INDEX_UNIQUE || $unique === true) {
125
            $index = false;
126
            $unique = true;
127
        } else {
128
            $index = $unique;
129
            $unique = false;
130
        }
131
132
        return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ') .
133
        $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
        $this->db->quoteTableName($table) .
135
        ($index !== false ? " USING $index" : '') .
0 ignored issues
show
introduced by
The condition $index !== false is always false.
Loading history...
136
        ' (' . $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
    public function dropIndex(string $name, string $table): string
148
    {
149
        if (\strpos($table, '.') !== false && \strpos($name, '.') === false) {
150
            if (\strpos($table, '{{') !== false) {
151
                $table = \preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $table);
152
                [$schema, $table] = \explode('.', $table);
153
                if (\strpos($schema, '%') === false) {
154
                    $name = $schema . '.' . $name;
155
                } else {
156
                    $name = '{{' . $schema . '.' . $name . '}}';
157
                }
158
            } else {
159
                [$schema] = \explode('.', $table);
160
                $name = $schema . '.' . $name;
161
            }
162
        }
163
164
        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
    public function renameTable(string $oldName, string $newName): string
176
    {
177
        return 'ALTER TABLE ' . $this->db->quoteTableName($oldName) . ' RENAME TO '
178
            . $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
    public function resetSequence(string $tableName, $value = null): string
196
    {
197
        $table = $this->db->getTableSchema($tableName);
198
        if ($table !== null && $table->getSequenceName() !== null) {
199
            /** c.f. http://www.postgresql.org/docs/8.1/static/functions-sequence.html */
200
            $sequence = $this->db->quoteTableName($table->getSequenceName());
201
            $tableName = $this->db->quoteTableName($tableName);
202
            if ($value === null) {
203
                $pk = $table->getPrimaryKey();
204
                $key = $this->db->quoteColumnName(reset($pk));
205
                $value = "(SELECT COALESCE(MAX({$key}),0) FROM {$tableName})+1";
206
            } else {
207
                $value = (int) $value;
208
            }
209
210
            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
    public function truncateTable(string $table): string
258
    {
259
        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
    public function alterColumn(string $table, string $column, $type): string
276
    {
277
        $columnName = $this->db->quoteColumnName($column);
278
        $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
        if (\preg_match('/^(DROP|SET|RESET)\s+/i', (string) $type)) {
285
            return "ALTER TABLE {$tableName} ALTER COLUMN {$columnName} {$type}";
286
        }
287
288
        $type = 'TYPE ' . $this->getColumnType($type);
289
290
        $multiAlterStatement = [];
291
        $constraintPrefix = \preg_replace('/[^a-z0-9_]/i', '', $table . '_' . $column);
292
293
        if (\preg_match('/\s+DEFAULT\s+(["\']?\w*["\']?)/i', $type, $matches)) {
294
            $type = \preg_replace('/\s+DEFAULT\s+(["\']?\w*["\']?)/i', '', $type);
295
            $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
            $multiAlterStatement[] = "ALTER COLUMN {$columnName} DROP DEFAULT";
299
        }
300
301
        $type = \preg_replace('/\s+NOT\s+NULL/i', '', $type, -1, $count);
302
        if ($count) {
303
            $multiAlterStatement[] = "ALTER COLUMN {$columnName} SET NOT NULL";
304
        } else {
305
            // remove additional null if any
306
            $type = \preg_replace('/\s+NULL/i', '', $type);
307
            // safe to drop not null even if there was none in the first place
308
            $multiAlterStatement[] = "ALTER COLUMN {$columnName} DROP NOT NULL";
309
        }
310
311
        if (\preg_match('/\s+CHECK\s+\((.+)\)/i', $type, $matches)) {
312
            $type = \preg_replace('/\s+CHECK\s+\((.+)\)/i', '', $type);
313
            $multiAlterStatement[] = "ADD CONSTRAINT {$constraintPrefix}_check CHECK ({$matches[1]})";
314
        }
315
316
        $type = \preg_replace('/\s+UNIQUE/i', '', $type, -1, $count);
317
        if ($count) {
318
            $multiAlterStatement[] = "ADD UNIQUE ({$columnName})";
319
        }
320
321
        // add what's left at the beginning
322
        \array_unshift($multiAlterStatement, "ALTER COLUMN {$columnName} {$type}");
323
324
        return 'ALTER TABLE ' . $tableName . ' ' . \implode(', ', $multiAlterStatement);
325
    }
326
327
    /**
328
     * {@inheritdoc}
329
     */
330
    public function insert(string $table, $columns, array &$params = []): string
331
    {
332
        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
    public function upsert(string $table, $insertColumns, $updateColumns, array &$params = []): string
342
    {
343
        $insertColumns = $this->normalizeTableRowData($table, $insertColumns);
344
345
        if (!\is_bool($updateColumns)) {
346
            $updateColumns = $this->normalizeTableRowData($table, $updateColumns);
347
        }
348
        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
        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
    private function newUpsert($table, $insertColumns, $updateColumns, &$params): string
366
    {
367
        $insertSql = $this->insert($table, $insertColumns, $params);
368
        [$uniqueNames, , $updateNames] = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns);
369
370
        if (empty($uniqueNames)) {
371
            return $insertSql;
372
        }
373
374
        if ($updateNames === []) {
375
            // there are no columns to update
376
            $updateColumns = false;
377
        }
378
379
        if ($updateColumns === false) {
380
            return "$insertSql ON CONFLICT DO NOTHING";
381
        }
382
383
        if ($updateColumns === true) {
384
            $updateColumns = [];
385
            foreach ($updateNames as $name) {
386
                $updateColumns[$name] = new Expression('EXCLUDED.' . $this->db->quoteColumnName($name));
387
            }
388
        }
389
390
        [$updates, $params] = $this->prepareUpdateSets($table, $updateColumns, $params);
391
392
        return $insertSql . ' ON CONFLICT (' . implode(', ', $uniqueNames) . ') DO UPDATE SET '
393
            . \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
    public function update(string $table, array $columns, $condition, array &$params = []): string
512
    {
513
        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
    private function normalizeTableRowData($table, $columns)
527
    {
528
        if ($columns instanceof Query) {
529
            return $columns;
530
        }
531
532
        if (($tableSchema = $this->db->getSchema()->getTableSchema($table)) !== null) {
533
            $columnSchemas = $tableSchema->getColumns();
534
            foreach ($columns as $name => $value) {
535
                if (isset($columnSchemas[$name]) && $columnSchemas[$name]->getType() === Schema::TYPE_BINARY && \is_string($value)) {
536
                    /** explicitly setup PDO param type for binary column */
537
                    $columns[$name] = new PdoValue($value, \PDO::PARAM_LOB);
538
                }
539
            }
540
        }
541
542
        return $columns;
543
    }
544
545
    /**
546
     * {@inheritdoc}
547
     */
548
    public function batchInsert(string $table, array $columns, $rows, array &$params = []): string
549
    {
550
        if (empty($rows)) {
551
            return '';
552
        }
553
554
        $schema = $this->db->getSchema();
555
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
556
            $columnSchemas = $tableSchema->getColumns();
557
        } else {
558
            $columnSchemas = [];
559
        }
560
561
        $values = [];
562
        foreach ($rows as $row) {
563
            $vs = [];
564
            foreach ($row as $i => $value) {
565
                if (isset($columns[$i], $columnSchemas[$columns[$i]])) {
566
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
567
                }
568
                if (\is_string($value)) {
569
                    $value = $schema->quoteValue($value);
570
                } elseif (\is_float($value)) {
571
                    // ensure type cast always has . as decimal separator in all locales
572
                    $value = StringHelper::floatToString($value);
573
                } elseif ($value === true) {
574
                    $value = 'TRUE';
575
                } elseif ($value === false) {
576
                    $value = 'FALSE';
577
                } elseif ($value === null) {
578
                    $value = 'NULL';
579
                } elseif ($value instanceof ExpressionInterface) {
580
                    $value = $this->buildExpression($value, $params);
581
                }
582
                $vs[] = $value;
583
            }
584
            $values[] = '(' . \implode(', ', $vs) . ')';
585
        }
586
587
        if (empty($values)) {
588
            return '';
589
        }
590
591
        foreach ($columns as $i => $name) {
592
            $columns[$i] = $schema->quoteColumnName($name);
593
        }
594
595
        return 'INSERT INTO ' . $schema->quoteTableName($table)
596
        . ' (' . \implode(', ', $columns) . ') VALUES ' . \implode(', ', $values);
597
    }
598
}
599