Passed
Push — master ( 1265f7...db8617 )
by Alexander
01:41
created

QueryBuilder::batchInsert()   C

Complexity

Conditions 14
Paths 86

Size

Total Lines 57
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 123.4578

Importance

Changes 0
Metric Value
cc 14
eloc 34
c 0
b 0
f 0
nc 86
nop 4
dl 0
loc 57
ccs 6
cts 34
cp 0.1765
crap 123.4578
rs 6.2666

How to fix   Long Method    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\Sqlite\Query;
6
7
use Yiisoft\Db\Constraints\Constraint;
8
use Yiisoft\Db\Drivers\Connection;
9
use Yiisoft\Db\Exceptions\InvalidArgumentException;
10
use Yiisoft\Db\Exceptions\NotSupportedException;
11
use Yiisoft\Db\Expressions\Expression;
12
use Yiisoft\Db\Expressions\ExpressionInterface;
13
use Yiisoft\Db\Querys\Query;
14
use Yiisoft\Db\Querys\QueryBuilder as BaseQueryBuilder;
15
use Yiisoft\Db\Querys\Conditions\LikeCondition;
16
use Yiisoft\Db\Querys\Conditions\InCondition;
17
use Yiisoft\Db\Sqlite\Condition\LikeConditionBuilder;
18
use Yiisoft\Db\Sqlite\Condition\InConditionBuilder;
19
use Yiisoft\Db\Sqlite\Schema\Schema;
20
use Yiisoft\Strings\StringHelper;
21
22
/**
23
 * QueryBuilder is the query builder for SQLite databases.
24
 */
25
class QueryBuilder extends BaseQueryBuilder
26
{
27
    /**
28
     * @var array mapping from abstract column types (keys) to physical column types (values).
29
     */
30
    public array $typeMap = [
31
        Schema::TYPE_PK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
32
        Schema::TYPE_UPK => 'integer UNSIGNED PRIMARY KEY AUTOINCREMENT NOT NULL',
33
        Schema::TYPE_BIGPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
34
        Schema::TYPE_UBIGPK => 'integer UNSIGNED PRIMARY KEY AUTOINCREMENT NOT NULL',
35
        Schema::TYPE_CHAR => 'char(1)',
36
        Schema::TYPE_STRING => 'varchar(255)',
37
        Schema::TYPE_TEXT => 'text',
38
        Schema::TYPE_TINYINT => 'tinyint',
39
        Schema::TYPE_SMALLINT => 'smallint',
40
        Schema::TYPE_INTEGER => 'integer',
41
        Schema::TYPE_BIGINT => 'bigint',
42
        Schema::TYPE_FLOAT => 'float',
43
        Schema::TYPE_DOUBLE => 'double',
44
        Schema::TYPE_DECIMAL => 'decimal(10,0)',
45
        Schema::TYPE_DATETIME => 'datetime',
46
        Schema::TYPE_TIMESTAMP => 'timestamp',
47
        Schema::TYPE_TIME => 'time',
48
        Schema::TYPE_DATE => 'date',
49
        Schema::TYPE_BINARY => 'blob',
50
        Schema::TYPE_BOOLEAN => 'boolean',
51
        Schema::TYPE_MONEY => 'decimal(19,4)',
52
    ];
53
54
55
    /**
56
     * {@inheritdoc}
57
     */
58 192
    protected function defaultExpressionBuilders(): array
59
    {
60 192
        return array_merge(parent::defaultExpressionBuilders(), [
61 192
            LikeCondition::class => LikeConditionBuilder::class,
62
            InCondition::class => InConditionBuilder::class,
63
        ]);
64
    }
65
66
    /**
67
     * Generates a batch INSERT SQL statement.
68
     *
69
     * For example,
70
     *
71
     * ```php
72
     * $connection->createCommand()->batchInsert('user', ['name', 'age'], [
73
     *     ['Tom', 30],
74
     *     ['Jane', 20],
75
     *     ['Linda', 25],
76
     * ])->execute();
77
     * ```
78
     *
79
     * Note that the values in each row must match the corresponding column names.
80
     *
81
     * @param string $table the table that new rows will be inserted into.
82
     * @param array $columns the column names
83
     * @param array|\Generator $rows the rows to be batch inserted into the table
84
     *
85
     * @return string the batch INSERT SQL statement
86
     */
87 13
    public function batchInsert(string $table, array $columns, $rows, array &$params = []): string
88
    {
89 13
        if (empty($rows)) {
90 2
            return '';
91
        }
92
93
        // SQLite supports batch insert natively since 3.7.11
94
        // http://www.sqlite.org/releaselog/3_7_11.html
95 12
        $this->db->open(); // ensure pdo is not null
0 ignored issues
show
Bug introduced by
The method open() 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

95
        $this->db->/** @scrutinizer ignore-call */ 
96
                   open(); // ensure pdo is not null

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...
96
97 12
        if (version_compare($this->db->getServerVersion(), '3.7.11', '>=')) {
98 12
            return parent::batchInsert($table, $columns, $rows, $params);
99
        }
100
101
        $schema = $this->db->getSchema();
102
103
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
104
            $columnSchemas = $tableSchema->columns;
105
        } else {
106
            $columnSchemas = [];
107
        }
108
109
        $values = [];
110
111
        foreach ($rows as $row) {
112
            $vs = [];
113
            foreach ($row as $i => $value) {
114
                if (isset($columnSchemas[$columns[$i]])) {
115
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
116
                }
117
                if (is_string($value)) {
118
                    $value = $schema->quoteValue($value);
119
                } elseif (is_float($value)) {
120
                    // ensure type cast always has . as decimal separator in all locales
121
                    $value = StringHelper::floatToString($value);
122
                } elseif ($value === false) {
123
                    $value = 0;
124
                } elseif ($value === null) {
125
                    $value = 'NULL';
126
                } elseif ($value instanceof ExpressionInterface) {
127
                    $value = $this->buildExpression($value, $params);
128
                }
129
                $vs[] = $value;
130
            }
131
            $values[] = implode(', ', $vs);
132
        }
133
134
        if (empty($values)) {
135
            return '';
136
        }
137
138
        foreach ($columns as $i => $name) {
139
            $columns[$i] = $schema->quoteColumnName($name);
140
        }
141
142
        return 'INSERT INTO ' . $schema->quoteTableName($table)
143
        . ' (' . implode(', ', $columns) . ') SELECT ' . implode(' UNION SELECT ', $values);
144
    }
145
146
    /**
147
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
148
     *
149
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
150
     * or 1.
151
     *
152
     * @param string $tableName the name of the table whose primary key sequence will be reset
153
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, the next new
154
     * row's primary key will have a value 1.
155
     *
156
     * @throws InvalidArgumentException if the table does not exist or there is no sequence associated with the table.
157
     *
158
     * @return string the SQL statement for resetting sequence
159
     */
160 1
    public function resetSequence(string $tableName, $value = null): string
161
    {
162 1
        $db = $this->db;
163
164 1
        $table = $db->getTableSchema($tableName);
0 ignored issues
show
Bug introduced by
The method getTableSchema() does not exist on null. ( Ignorable by Annotation )

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

164
        /** @scrutinizer ignore-call */ 
165
        $table = $db->getTableSchema($tableName);

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

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

Loading history...
165
166 1
        if ($table !== null && $table->sequenceName !== null) {
167 1
            $tableName = $db->quoteTableName($tableName);
168 1
            if ($value === null) {
169 1
                $key = $this->db->quoteColumnName(reset($table->primaryKey));
170
                $value = $this->db->useMaster(function (Connection $db) use ($key, $tableName) {
171 1
                    return $db->createCommand("SELECT MAX($key) FROM $tableName")->queryScalar();
172 1
                });
173
            } else {
174 1
                $value = (int) $value - 1;
175
            }
176
177 1
            return "UPDATE sqlite_sequence SET seq='$value' WHERE name='{$table->name}'";
178
        } elseif ($table === null) {
179
            throw new InvalidArgumentException("Table not found: $tableName");
180
        }
181
182
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.'");
183
    }
184
185
    /**
186
     * Enables or disables integrity check.
187
     *
188
     * @param bool $check whether to turn on or off the integrity check.
189
     * @param string $schema the schema of the tables. Meaningless for SQLite.
190
     * @param string $table the table name. Meaningless for SQLite.
191
     *
192
     * @throws NotSupportedException this is not supported by SQLite
193
     *
194
     * @return string the SQL statement for checking integrity
195
     */
196
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
197
    {
198
        return 'PRAGMA foreign_keys=' . (int) $check;
199
    }
200
201
    /**
202
     * Builds a SQL statement for truncating a DB table.
203
     *
204
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
205
     *
206
     * @return string the SQL statement for truncating a DB table.
207
     */
208 1
    public function truncateTable(string $table): string
209
    {
210 1
        return 'DELETE FROM ' . $this->db->quoteTableName($table);
211
    }
212
213
    /**
214
     * Builds a SQL statement for dropping an index.
215
     *
216
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
217
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
218
     *
219
     * @return string the SQL statement for dropping an index.
220
     */
221 2
    public function dropIndex(string $name, string $table): string
222
    {
223 2
        return 'DROP INDEX ' . $this->db->quoteTableName($name);
224
    }
225
226
    /**
227
     * Builds a SQL statement for dropping a DB column.
228
     *
229
     * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
230
     * @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
231
     *
232
     * @throws NotSupportedException this is not supported by SQLite
233
     *
234
     * @return string the SQL statement for dropping a DB column.
235
     */
236
    public function dropColumn(string $table, string $column): string
237
    {
238
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
239
    }
240
241
    /**
242
     * Builds a SQL statement for renaming a column.
243
     *
244
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
245
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
246
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
247
     *
248
     * @throws NotSupportedException this is not supported by SQLite
249
     *
250
     * @return string the SQL statement for renaming a DB column.
251
     */
252
    public function renameColumn(string $table, string $oldName, string $newName): string
253
    {
254
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
255
    }
256
257
    /**
258
     * Builds a SQL statement for adding a foreign key constraint to an existing table.
259
     * The method will properly quote the table and column names.
260
     * @param string $name the name of the foreign key constraint.
261
     * @param string $table the table that the foreign key constraint will be added to.
262
     * @param string|array $columns the name of the column to that the constraint will be added on.
263
     * If there are multiple columns, separate them with commas or use an array to represent them.
264
     * @param string $refTable the table that the foreign key references to.
265
     * @param string|array $refColumns the name of the column that the foreign key references to.
266
     * If there are multiple columns, separate them with commas or use an array to represent them.
267
     * @param string $delete the ON DELETE option. Most DBMS support these options:
268
     * RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
269
     * @param string $update the ON UPDATE option. Most DBMS support these options:
270
     * RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
271
     *
272
     * @throws NotSupportedException this is not supported by SQLite
273
     *
274
     * @return string the SQL statement for adding a foreign key constraint to an existing table.
275
     */
276
    public function addForeignKey(
277
        string $name,
278
        string $table,
279
        $columns,
280
        string $refTable,
281
        $refColumns,
282
        ?string $delete = null,
283
        ?string $update = null
284
    ): string {
285
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
286
    }
287
288
    /**
289
     * Builds a SQL statement for dropping a foreign key constraint.
290
     * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted
291
     * by the method.
292
     * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
293
     *
294
     * @throws NotSupportedException this is not supported by SQLite
295
     *
296
     * @return string the SQL statement for dropping a foreign key constraint.
297
     */
298
    public function dropForeignKey(string $name, string $table): string
299
    {
300
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
301
    }
302
303
    /**
304
     * Builds a SQL statement for renaming a DB table.
305
     *
306
     * @param string $table the table to be renamed. The name will be properly quoted by the method.
307
     * @param string $newName the new table name. The name will be properly quoted by the method.
308
     *
309
     * @return string the SQL statement for renaming a DB table.
310
     */
311 3
    public function renameTable(string $table, string $newName): string
312
    {
313 3
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' RENAME TO ' . $this->db->quoteTableName($newName);
314
    }
315
316
    /**
317
     * Builds a SQL statement for changing the definition of a column.
318
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the
319
     * method.
320
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
321
     * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract
322
     * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept
323
     * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null'
324
     * will become 'varchar(255) not null'.
325
     *
326
     * @throws NotSupportedException this is not supported by SQLite
327
     *
328
     * @return string the SQL statement for changing the definition of a column.
329
     */
330
    public function alterColumn(string $table, string $column, string $type): string
331
    {
332
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
333
    }
334
335
    /**
336
     * Builds a SQL statement for adding a primary key constraint to an existing table.
337
     *
338
     * @param string $name the name of the primary key constraint.
339
     * @param string $table the table that the primary key constraint will be added to.
340
     * @param string|array $columns comma separated string or array of columns that the primary key will consist of.
341
     *
342
     * @throws NotSupportedException this is not supported by SQLite
343
     *
344
     * @return string the SQL statement for adding a primary key constraint to an existing table.
345
     */
346
    public function addPrimaryKey(string $name, string $table, $columns): string
347
    {
348
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
349
    }
350
351
    /**
352
     * Builds a SQL statement for removing a primary key constraint to an existing table.
353
     *
354
     * @param string $name the name of the primary key constraint to be removed.
355
     * @param string $table the table that the primary key constraint will be removed from.
356
     *
357
     * @throws NotSupportedException this is not supported by SQLite
358
     *
359
     * @return string the SQL statement for removing a primary key constraint from an existing table.
360
     */
361
    public function dropPrimaryKey(string $name, string $table): string
362
    {
363
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
364
    }
365
366
    /**
367
     * {@inheritdoc}
368
     *
369
     * @throws NotSupportedException this is not supported by SQLite.
370
     */
371
    public function addUnique(string $name, string $table, $columns): string
372
    {
373
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
374
    }
375
376
    /**
377
     * {@inheritdoc}
378
     *
379
     * @throws NotSupportedException this is not supported by SQLite.
380
     */
381
    public function dropUnique(string $name, string $table): string
382
    {
383
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
384
    }
385
386
    /**
387
     * {@inheritdoc}
388
     *
389
     * @throws NotSupportedException this is not supported by SQLite.
390
     */
391
    public function addCheck(string $name, string $table, string $expression): string
392
    {
393
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
394
    }
395
396
    /**
397
     * {@inheritdoc}
398
     *
399
     * @throws NotSupportedException this is not supported by SQLite.
400
     */
401
    public function dropCheck(string $name, string $table): string
402
    {
403
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
404
    }
405
406
    /**
407
     * {@inheritdoc}
408
     *
409
     * @throws NotSupportedException this is not supported by SQLite.
410
     */
411
    public function addDefaultValue(string $name, string $table, string $column, $value): string
412
    {
413
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
414
    }
415
416
    /**
417
     * {@inheritdoc}
418
     *
419
     * @throws NotSupportedException this is not supported by SQLite.
420
     */
421
    public function dropDefaultValue(string $name, string $table): string
422
    {
423
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
424
    }
425
426
    /**
427
     * {@inheritdoc}
428
     *
429
     * @throws NotSupportedException
430
     */
431
    public function addCommentOnColumn(string $table, string $column, string $comment): string
432
    {
433
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
434
    }
435
436
    /**
437
     * {@inheritdoc}
438
     *
439
     * @throws NotSupportedException
440
     */
441
    public function addCommentOnTable(string $table, string $comment): string
442
    {
443
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
444
    }
445
446
    /**
447
     * {@inheritdoc}
448
     *
449
     * @throws NotSupportedException
450
     */
451
    public function dropCommentFromColumn(string $table, string $column): string
452
    {
453
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
454
    }
455
456
    /**
457
     * {@inheritdoc}
458
     *
459
     * @throws NotSupportedException
460
     */
461
    public function dropCommentFromTable(string $table): string
462
    {
463
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
464
    }
465
466
    /**
467
     * {@inheritdoc}
468
     */
469 136
    public function buildLimit($limit, $offset): string
470
    {
471 136
        $sql = '';
472
473 136
        if ($this->hasLimit($limit)) {
474 8
            $sql = 'LIMIT ' . $limit;
475 8
            if ($this->hasOffset($offset)) {
476 8
                $sql .= ' OFFSET ' . $offset;
477
            }
478 131
        } elseif ($this->hasOffset($offset)) {
479
            // limit is not optional in SQLite
480
            // http://www.sqlite.org/syntaxdiagrams.html#select-stmt
481
            $sql = "LIMIT 9223372036854775807 OFFSET $offset"; // 2^63-1
482
        }
483
484 136
        return $sql;
485
    }
486
487
    /**
488
     * {@inheritdoc}
489
     */
490 136
    public function build(Query $query, array $params = []): array
491
    {
492 136
        $query = $query->prepare($this);
493
494 136
        $params = empty($params) ? $query->params : array_merge($params, $query->params);
495
496
        $clauses = [
497 136
            $this->buildSelect($query->select, $params, $query->distinct, $query->selectOption),
498 136
            $this->buildFrom($query->from, $params),
0 ignored issues
show
Bug introduced by
It seems like $query->from can also be of type string; however, parameter $tables of Yiisoft\Db\Querys\QueryBuilder::buildFrom() does only seem to accept array|null, 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

498
            $this->buildFrom(/** @scrutinizer ignore-type */ $query->from, $params),
Loading history...
499 136
            $this->buildJoin($query->join, $params),
500 136
            $this->buildWhere($query->where, $params),
501 136
            $this->buildGroupBy($query->groupBy),
502 136
            $this->buildHaving($query->having, $params),
0 ignored issues
show
Bug introduced by
It seems like $query->having can also be of type Yiisoft\Db\Expressions\ExpressionInterface; however, parameter $condition of Yiisoft\Db\Querys\QueryBuilder::buildHaving() does only seem to accept array|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

502
            $this->buildHaving(/** @scrutinizer ignore-type */ $query->having, $params),
Loading history...
503
        ];
504
505 136
        $sql = implode($this->separator, array_filter($clauses));
506 136
        $sql = $this->buildOrderByAndLimit($sql, $query->orderBy, $query->limit, $query->offset);
507
508 136
        if (!empty($query->orderBy)) {
509 5
            foreach ($query->orderBy as $expression) {
510 5
                if ($expression instanceof ExpressionInterface) {
511 1
                    $this->buildExpression($expression, $params);
512
                }
513
            }
514
        }
515
516 136
        if (!empty($query->groupBy)) {
517 2
            foreach ($query->groupBy as $expression) {
518 2
                if ($expression instanceof ExpressionInterface) {
519 1
                    $this->buildExpression($expression, $params);
520
                }
521
            }
522
        }
523
524 136
        $union = $this->buildUnion($query->union, $params);
525
526 136
        if ($union !== '') {
527 3
            $sql = "$sql{$this->separator}$union";
528
        }
529
530 136
        $with = $this->buildWithQueries($query->withQueries, $params);
531
532 136
        if ($with !== '') {
533 2
            $sql = "$with{$this->separator}$sql";
534
        }
535
536 136
        return [$sql, $params];
537
    }
538
539 6
    public function createIndex(string $name, string $table, $columns, bool $unique = false): string
540
    {
541 6
        $sql = ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ')
542 6
            . $this->db->quoteTableName($name) . ' ON '
543 6
            . $this->db->quoteTableName($table)
544 6
            . ' (' . $this->buildColumns($columns) . ')';
545
546 6
        $sql = preg_replace_callback(
547 6
            '/(`.*`) ON ({{(%?)([\w\-]+)}\}\.{{((%?)[\w\-]+)\\}\\})|(`.*`) ON ({{(%?)([\w\-]+)\.([\w\-]+)\\}\\})/',
548
            static function ($matches) {
549
                if (!empty($matches[1])) {
550
                    return $matches[4].".".$matches[1]
551
                     . ' ON {{' .$matches[3].$matches[5] . '}}';
552
                }
553
554
                if (!empty($matches[7])) {
555
                    return $matches[10]. '.' .$matches[7]
556
                     . ' ON {{' .$matches[9].$matches[11] . '}}';
557
                }
558 6
            },
559
            $sql
560
        );
561
562 6
        return $sql;
563
    }
564
565
    /**
566
     * @param array $unions
567
     * @param array $params the binding parameters to be populated
568
     *
569
     * @return string the UNION clause built from {@see Query::$union}.
570
     */
571 136
    public function buildUnion(array $unions, array &$params): string
572
    {
573 136
        if (empty($unions)) {
574 136
            return '';
575
        }
576
577 3
        $result = '';
578
579 3
        foreach ($unions as $i => $union) {
580 3
            $query = $union['query'];
581 3
            if ($query instanceof Query) {
582 3
                [$unions[$i]['query'], $params] = $this->build($query, $params);
583
            }
584
585 3
            $result .= ' UNION ' . ($union['all'] ? 'ALL ' : '') . ' ' . $unions[$i]['query'];
586
        }
587
588 3
        return trim($result);
589
    }
590
591
    /**
592
     * {@inheritdoc}
593
     * @see https://stackoverflow.com/questions/15277373/sqlite-upsert-update-or-insert/15277374#15277374
594
     */
595 18
    public function upsert(string $table, $insertColumns, $updateColumns, array &$params): string
596
    {
597
        /** @var Constraint[] $constraints */
598 18
        $constraints = [];
599
600 18
        [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns($table, $insertColumns, $updateColumns, $constraints);
601
602 18
        if (empty($uniqueNames)) {
603 3
            return $this->insert($table, $insertColumns, $params);
604
        }
605
606 15
        [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);
607
608 15
        $insertSql = 'INSERT OR IGNORE INTO ' . $this->db->quoteTableName($table)
609 15
            . (!empty($insertNames) ? ' (' . implode(', ', $insertNames) . ')' : '')
610 15
            . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
611
612 15
        if ($updateColumns === false) {
613 5
            return $insertSql;
614
        }
615
616 10
        $updateCondition = ['or'];
617 10
        $quotedTableName = $this->db->quoteTableName($table);
618
619 10
        foreach ($constraints as $constraint) {
620 10
            $constraintCondition = ['and'];
621 10
            foreach ($constraint->getColumnNames() as $name) {
622 10
                $quotedName = $this->db->quoteColumnName($name);
623 10
                $constraintCondition[] = "$quotedTableName.$quotedName=(SELECT $quotedName FROM `EXCLUDED`)";
624
            }
625 10
            $updateCondition[] = $constraintCondition;
626
        }
627
628 10
        if ($updateColumns === true) {
629 4
            $updateColumns = [];
630 4
            foreach ($updateNames as $name) {
631 4
                $quotedName = $this->db->quoteColumnName($name);
632
633 4
                if (strrpos($quotedName, '.') === false) {
634 4
                    $quotedName = "(SELECT $quotedName FROM `EXCLUDED`)";
635
                }
636 4
                $updateColumns[$name] = new Expression($quotedName);
637
            }
638
        }
639
640 10
        $updateSql = 'WITH "EXCLUDED" (' . implode(', ', $insertNames)
641 10
            . ') AS (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' ')) . ') '
642 10
            . $this->update($table, $updateColumns, $updateCondition, $params);
643
644 10
        return "$updateSql; $insertSql;";
645
    }
646
}
647