Passed
Push — master ( d42673...c7102b )
by Wilmer
09:45 queued 08:06
created

QueryBuilder   F

Complexity

Total Complexity 75

Size/Duplication

Total Lines 626
Duplicated Lines 0 %

Test Coverage

Coverage 60%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 187
c 2
b 0
f 0
dl 0
loc 626
ccs 111
cts 185
cp 0.6
rs 2.4
wmc 75

29 Methods

Rating   Name   Duplication   Size   Complexity  
C batchInsert() 0 57 14
A defaultExpressionBuilders() 0 5 1
A addCommentOnTable() 0 3 1
A renameColumn() 0 3 1
A resetSequence() 0 24 5
A addUnique() 0 3 1
A renameTable() 0 3 1
A dropIndex() 0 3 1
A addPrimaryKey() 0 3 1
A truncateTable() 0 3 1
A dropForeignKey() 0 3 1
A addForeignKey() 0 10 1
A alterColumn() 0 3 1
A checkIntegrity() 0 3 1
A addCheck() 0 3 1
A buildUnion() 0 18 5
B build() 0 47 10
A addDefaultValue() 0 3 1
A dropCommentFromColumn() 0 3 1
A dropColumn() 0 3 1
A buildLimit() 0 16 4
A dropCheck() 0 3 1
B upsert() 0 55 11
A dropDefaultValue() 0 3 1
A dropCommentFromTable() 0 3 1
A dropPrimaryKey() 0 3 1
A dropUnique() 0 3 1
A addCommentOnColumn() 0 3 1
A createIndex() 0 24 4

How to fix   Complexity   

Complex Class

Complex classes like QueryBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use QueryBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Sqlite\Query;
6
7
use Yiisoft\Db\Constraint\Constraint;
8
use Yiisoft\Db\Connection\Connection;
9
use Yiisoft\Db\Exception\InvalidArgumentException;
10
use Yiisoft\Db\Exception\NotSupportedException;
11
use Yiisoft\Db\Expression\Expression;
12
use Yiisoft\Db\Expression\ExpressionInterface;
13
use Yiisoft\Db\Query\Query;
14
use Yiisoft\Db\Query\QueryBuilder as BaseQueryBuilder;
15
use Yiisoft\Db\Query\Conditions\LikeCondition;
16
use Yiisoft\Db\Query\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
    protected 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;
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...
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->getSequenceName() !== null) {
167 1
            $tableName = $db->quoteTableName($tableName);
168 1
            if ($value === null) {
169 1
                $pk = $table->getPrimaryKey();
170 1
                $key = $this->db->quoteColumnName(reset($pk));
171
                $value = $this->db->useMaster(function (Connection $db) use ($key, $tableName) {
172 1
                    return $db->createCommand("SELECT MAX($key) FROM $tableName")->queryScalar();
173 1
                });
174
            } else {
175 1
                $value = (int) $value - 1;
176
            }
177
178 1
            return "UPDATE sqlite_sequence SET seq='$value' WHERE name='{$table->getName()}'";
179
        } elseif ($table === null) {
180
            throw new InvalidArgumentException("Table not found: $tableName");
181
        }
182
183
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.'");
184
    }
185
186
    /**
187
     * Enables or disables integrity check.
188
     *
189
     * @param bool $check whether to turn on or off the integrity check.
190
     * @param string $schema the schema of the tables. Meaningless for SQLite.
191
     * @param string $table the table name. Meaningless for SQLite.
192
     *
193
     * @throws NotSupportedException this is not supported by SQLite
194
     *
195
     * @return string the SQL statement for checking integrity
196
     */
197
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
198
    {
199
        return 'PRAGMA foreign_keys=' . (int) $check;
200
    }
201
202
    /**
203
     * Builds a SQL statement for truncating a DB table.
204
     *
205
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
206
     *
207
     * @return string the SQL statement for truncating a DB table.
208
     */
209 1
    public function truncateTable(string $table): string
210
    {
211 1
        return 'DELETE FROM ' . $this->db->quoteTableName($table);
212
    }
213
214
    /**
215
     * Builds a SQL statement for dropping an index.
216
     *
217
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
218
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
219
     *
220
     * @return string the SQL statement for dropping an index.
221
     */
222 2
    public function dropIndex(string $name, string $table): string
223
    {
224 2
        return 'DROP INDEX ' . $this->db->quoteTableName($name);
225
    }
226
227
    /**
228
     * Builds a SQL statement for dropping a DB column.
229
     *
230
     * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
231
     * @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
232
     *
233
     * @throws NotSupportedException this is not supported by SQLite
234
     *
235
     * @return string the SQL statement for dropping a DB column.
236
     */
237
    public function dropColumn(string $table, string $column): string
238
    {
239
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
240
    }
241
242
    /**
243
     * Builds a SQL statement for renaming a column.
244
     *
245
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
246
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
247
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
248
     *
249
     * @throws NotSupportedException this is not supported by SQLite
250
     *
251
     * @return string the SQL statement for renaming a DB column.
252
     */
253
    public function renameColumn(string $table, string $oldName, string $newName): string
254
    {
255
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
256
    }
257
258
    /**
259
     * Builds a SQL statement for adding a foreign key constraint to an existing table.
260
     * The method will properly quote the table and column names.
261
     * @param string $name the name of the foreign key constraint.
262
     * @param string $table the table that the foreign key constraint will be added to.
263
     * @param string|array $columns the name of the column to that the constraint will be added on.
264
     * If there are multiple columns, separate them with commas or use an array to represent them.
265
     * @param string $refTable the table that the foreign key references to.
266
     * @param string|array $refColumns the name of the column that the foreign key references to.
267
     * If there are multiple columns, separate them with commas or use an array to represent them.
268
     * @param string $delete the ON DELETE option. Most DBMS support these options:
269
     * RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
270
     * @param string $update the ON UPDATE option. Most DBMS support these options:
271
     * RESTRICT, CASCADE, NO ACTION, SET DEFAULT, SET NULL
272
     *
273
     * @throws NotSupportedException this is not supported by SQLite
274
     *
275
     * @return string the SQL statement for adding a foreign key constraint to an existing table.
276
     */
277
    public function addForeignKey(
278
        string $name,
279
        string $table,
280
        $columns,
281
        string $refTable,
282
        $refColumns,
283
        ?string $delete = null,
284
        ?string $update = null
285
    ): string {
286
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
287
    }
288
289
    /**
290
     * Builds a SQL statement for dropping a foreign key constraint.
291
     * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted
292
     * by the method.
293
     * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
294
     *
295
     * @throws NotSupportedException this is not supported by SQLite
296
     *
297
     * @return string the SQL statement for dropping a foreign key constraint.
298
     */
299
    public function dropForeignKey(string $name, string $table): string
300
    {
301
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
302
    }
303
304
    /**
305
     * Builds a SQL statement for renaming a DB table.
306
     *
307
     * @param string $table the table to be renamed. The name will be properly quoted by the method.
308
     * @param string $newName the new table name. The name will be properly quoted by the method.
309
     *
310
     * @return string the SQL statement for renaming a DB table.
311
     */
312 3
    public function renameTable(string $table, string $newName): string
313
    {
314 3
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' RENAME TO ' . $this->db->quoteTableName($newName);
315
    }
316
317
    /**
318
     * Builds a SQL statement for changing the definition of a column.
319
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the
320
     * method.
321
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
322
     * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract
323
     * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept
324
     * in the generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null'
325
     * will become 'varchar(255) not null'.
326
     *
327
     * @throws NotSupportedException this is not supported by SQLite
328
     *
329
     * @return string the SQL statement for changing the definition of a column.
330
     */
331
    public function alterColumn(string $table, string $column, string $type): string
332
    {
333
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
334
    }
335
336
    /**
337
     * Builds a SQL statement for adding a primary key constraint to an existing table.
338
     *
339
     * @param string $name the name of the primary key constraint.
340
     * @param string $table the table that the primary key constraint will be added to.
341
     * @param string|array $columns comma separated string or array of columns that the primary key will consist of.
342
     *
343
     * @throws NotSupportedException this is not supported by SQLite
344
     *
345
     * @return string the SQL statement for adding a primary key constraint to an existing table.
346
     */
347
    public function addPrimaryKey(string $name, string $table, $columns): string
348
    {
349
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
350
    }
351
352
    /**
353
     * Builds a SQL statement for removing a primary key constraint to an existing table.
354
     *
355
     * @param string $name the name of the primary key constraint to be removed.
356
     * @param string $table the table that the primary key constraint will be removed from.
357
     *
358
     * @throws NotSupportedException this is not supported by SQLite
359
     *
360
     * @return string the SQL statement for removing a primary key constraint from an existing table.
361
     */
362
    public function dropPrimaryKey(string $name, string $table): string
363
    {
364
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
365
    }
366
367
    /**
368
     * {@inheritdoc}
369
     *
370
     * @throws NotSupportedException this is not supported by SQLite.
371
     */
372
    public function addUnique(string $name, string $table, $columns): string
373
    {
374
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
375
    }
376
377
    /**
378
     * {@inheritdoc}
379
     *
380
     * @throws NotSupportedException this is not supported by SQLite.
381
     */
382
    public function dropUnique(string $name, string $table): string
383
    {
384
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
385
    }
386
387
    /**
388
     * {@inheritdoc}
389
     *
390
     * @throws NotSupportedException this is not supported by SQLite.
391
     */
392
    public function addCheck(string $name, string $table, string $expression): string
393
    {
394
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
395
    }
396
397
    /**
398
     * {@inheritdoc}
399
     *
400
     * @throws NotSupportedException this is not supported by SQLite.
401
     */
402
    public function dropCheck(string $name, string $table): string
403
    {
404
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
405
    }
406
407
    /**
408
     * {@inheritdoc}
409
     *
410
     * @throws NotSupportedException this is not supported by SQLite.
411
     */
412
    public function addDefaultValue(string $name, string $table, string $column, $value): string
413
    {
414
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
415
    }
416
417
    /**
418
     * {@inheritdoc}
419
     *
420
     * @throws NotSupportedException this is not supported by SQLite.
421
     */
422
    public function dropDefaultValue(string $name, string $table): string
423
    {
424
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
425
    }
426
427
    /**
428
     * {@inheritdoc}
429
     *
430
     * @throws NotSupportedException
431
     */
432
    public function addCommentOnColumn(string $table, string $column, string $comment): string
433
    {
434
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
435
    }
436
437
    /**
438
     * {@inheritdoc}
439
     *
440
     * @throws NotSupportedException
441
     */
442
    public function addCommentOnTable(string $table, string $comment): string
443
    {
444
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
445
    }
446
447
    /**
448
     * {@inheritdoc}
449
     *
450
     * @throws NotSupportedException
451
     */
452
    public function dropCommentFromColumn(string $table, string $column): string
453
    {
454
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
455
    }
456
457
    /**
458
     * {@inheritdoc}
459
     *
460
     * @throws NotSupportedException
461
     */
462
    public function dropCommentFromTable(string $table): string
463
    {
464
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
465
    }
466
467
    /**
468
     * {@inheritdoc}
469
     */
470 136
    public function buildLimit($limit, $offset): string
471
    {
472 136
        $sql = '';
473
474 136
        if ($this->hasLimit($limit)) {
475 8
            $sql = 'LIMIT ' . $limit;
476 8
            if ($this->hasOffset($offset)) {
477 8
                $sql .= ' OFFSET ' . $offset;
478
            }
479 131
        } elseif ($this->hasOffset($offset)) {
480
            // limit is not optional in SQLite
481
            // http://www.sqlite.org/syntaxdiagrams.html#select-stmt
482
            $sql = "LIMIT 9223372036854775807 OFFSET $offset"; // 2^63-1
483
        }
484
485 136
        return $sql;
486
    }
487
488
    /**
489
     * {@inheritdoc}
490
     */
491 136
    public function build(Query $query, array $params = []): array
492
    {
493 136
        $query = $query->prepare($this);
494
495 136
        $params = empty($params) ? $query->getParams() : array_merge($params, $query->getParams());
496
497
        $clauses = [
498 136
            $this->buildSelect($query->getSelect(), $params, $query->getDistinct(), $query->getSelectOption()),
499 136
            $this->buildFrom($query->getFrom(), $params),
500 136
            $this->buildJoin($query->getJoin(), $params),
501 136
            $this->buildWhere($query->getWhere(), $params),
502 136
            $this->buildGroupBy($query->getGroupBy()),
503 136
            $this->buildHaving($query->getHaving(), $params),
504
        ];
505
506 136
        $sql = implode($this->separator, array_filter($clauses));
507 136
        $sql = $this->buildOrderByAndLimit($sql, $query->getOrderBy(), $query->getLimit(), $query->getOffset());
508
509 136
        if (!empty($query->getOrderBy())) {
510 5
            foreach ($query->getOrderBy() as $expression) {
511 5
                if ($expression instanceof ExpressionInterface) {
512 1
                    $this->buildExpression($expression, $params);
513
                }
514
            }
515
        }
516
517 136
        if (!empty($query->getGroupBy())) {
518 2
            foreach ($query->getGroupBy() as $expression) {
519 2
                if ($expression instanceof ExpressionInterface) {
520 1
                    $this->buildExpression($expression, $params);
521
                }
522
            }
523
        }
524
525 136
        $union = $this->buildUnion($query->getUnion(), $params);
526
527 136
        if ($union !== '') {
528 3
            $sql = "$sql{$this->separator}$union";
529
        }
530
531 136
        $with = $this->buildWithQueries($query->getWithQueries(), $params);
532
533 136
        if ($with !== '') {
534 2
            $sql = "$with{$this->separator}$sql";
535
        }
536
537 136
        return [$sql, $params];
538
    }
539
540 6
    public function createIndex(string $name, string $table, $columns, bool $unique = false): string
541
    {
542 6
        $sql = ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ')
543 6
            . $this->db->quoteTableName($name) . ' ON '
544 6
            . $this->db->quoteTableName($table)
545 6
            . ' (' . $this->buildColumns($columns) . ')';
546
547 6
        $sql = preg_replace_callback(
548 6
            '/(`.*`) ON ({{(%?)([\w\-]+)}\}\.{{((%?)[\w\-]+)\\}\\})|(`.*`) ON ({{(%?)([\w\-]+)\.([\w\-]+)\\}\\})/',
549
            static function ($matches) {
550
                if (!empty($matches[1])) {
551
                    return $matches[4] . "." . $matches[1]
552
                     . ' ON {{' . $matches[3] . $matches[5] . '}}';
553
                }
554
555
                if (!empty($matches[7])) {
556
                    return $matches[10] . '.' . $matches[7]
557
                     . ' ON {{' . $matches[9] . $matches[11] . '}}';
558
                }
559 6
            },
560
            $sql
561
        );
562
563 6
        return $sql;
564
    }
565
566
    /**
567
     * @param array $unions
568
     * @param array $params the binding parameters to be populated
569
     *
570
     * @return string the UNION clause built from {@see Query::$union}.
571
     */
572 136
    public function buildUnion(array $unions, array &$params): string
573
    {
574 136
        if (empty($unions)) {
575 136
            return '';
576
        }
577
578 3
        $result = '';
579
580 3
        foreach ($unions as $i => $union) {
581 3
            $query = $union['query'];
582 3
            if ($query instanceof Query) {
583 3
                [$unions[$i]['query'], $params] = $this->build($query, $params);
584
            }
585
586 3
            $result .= ' UNION ' . ($union['all'] ? 'ALL ' : '') . ' ' . $unions[$i]['query'];
587
        }
588
589 3
        return trim($result);
590
    }
591
592
    /**
593
     * {@inheritdoc}
594
     * @see https://stackoverflow.com/questions/15277373/sqlite-upsert-update-or-insert/15277374#15277374
595
     */
596 18
    public function upsert(string $table, $insertColumns, $updateColumns, array &$params): string
597
    {
598
        /** @var Constraint[] $constraints */
599 18
        $constraints = [];
600
601 18
        [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns(
602 18
            $table,
603
            $insertColumns,
604
            $updateColumns,
605
            $constraints
606
        );
607
608 18
        if (empty($uniqueNames)) {
609 3
            return $this->insert($table, $insertColumns, $params);
610
        }
611
612 15
        [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);
613
614 15
        $insertSql = 'INSERT OR IGNORE INTO ' . $this->db->quoteTableName($table)
615 15
            . (!empty($insertNames) ? ' (' . implode(', ', $insertNames) . ')' : '')
616 15
            . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
617
618 15
        if ($updateColumns === false) {
619 5
            return $insertSql;
620
        }
621
622 10
        $updateCondition = ['or'];
623 10
        $quotedTableName = $this->db->quoteTableName($table);
624
625 10
        foreach ($constraints as $constraint) {
626 10
            $constraintCondition = ['and'];
627 10
            foreach ($constraint->getColumnNames() as $name) {
628 10
                $quotedName = $this->db->quoteColumnName($name);
629 10
                $constraintCondition[] = "$quotedTableName.$quotedName=(SELECT $quotedName FROM `EXCLUDED`)";
630
            }
631 10
            $updateCondition[] = $constraintCondition;
632
        }
633
634 10
        if ($updateColumns === true) {
635 4
            $updateColumns = [];
636 4
            foreach ($updateNames as $name) {
637 4
                $quotedName = $this->db->quoteColumnName($name);
638
639 4
                if (strrpos($quotedName, '.') === false) {
640 4
                    $quotedName = "(SELECT $quotedName FROM `EXCLUDED`)";
641
                }
642 4
                $updateColumns[$name] = new Expression($quotedName);
643
            }
644
        }
645
646 10
        $updateSql = 'WITH "EXCLUDED" (' . implode(', ', $insertNames)
647 10
            . ') AS (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')' : ltrim($values, ' '))
648 10
            . ') ' . $this->update($table, $updateColumns, $updateCondition, $params);
649
650 10
        return "$updateSql; $insertSql;";
651
    }
652
}
653