Passed
Push — master ( 3cf8ec...4591f3 )
by Wilmer
16:32 queued 14:58
created

QueryBuilder   F

Complexity

Total Complexity 75

Size/Duplication

Total Lines 808
Duplicated Lines 0 %

Test Coverage

Coverage 60.22%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 187
dl 0
loc 808
ccs 112
cts 186
cp 0.6022
rs 2.4
c 2
b 0
f 0
wmc 75

29 Methods

Rating   Name   Duplication   Size   Complexity  
A dropPrimaryKey() 0 3 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 19 4
A dropCheck() 0 3 1
B upsert() 0 55 11
C batchInsert() 0 60 14
A dropDefaultValue() 0 3 1
A dropCommentFromTable() 0 3 1
A dropUnique() 0 3 1
A addCommentOnColumn() 0 3 1
A defaultExpressionBuilders() 0 5 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\Connection\Connection;
8
use Yiisoft\Db\Constraint\Constraint;
9
use Yiisoft\Db\Exception\Exception;
10
use Yiisoft\Db\Exception\InvalidArgumentException;
11
use Yiisoft\Db\Exception\InvalidConfigException;
12
use Yiisoft\Db\Exception\NotSupportedException;
13
use Yiisoft\Db\Expression\Expression;
14
use Yiisoft\Db\Expression\ExpressionInterface;
15
use Yiisoft\Db\Query\Conditions\InCondition;
16
use Yiisoft\Db\Query\Conditions\LikeCondition;
17
use Yiisoft\Db\Query\Query;
18
use Yiisoft\Db\Query\QueryBuilder as BaseQueryBuilder;
19
use Yiisoft\Db\Sqlite\Condition\InConditionBuilder;
20
use Yiisoft\Db\Sqlite\Condition\LikeConditionBuilder;
21
use Yiisoft\Db\Sqlite\Schema\Schema;
22
use Yiisoft\Strings\StringHelper;
23
24
class QueryBuilder extends BaseQueryBuilder
25
{
26
    /**
27
     * @var array mapping from abstract column types (keys) to physical column types (values).
28
     */
29
    protected array $typeMap = [
30
        Schema::TYPE_PK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
31
        Schema::TYPE_UPK => 'integer UNSIGNED PRIMARY KEY AUTOINCREMENT NOT NULL',
32
        Schema::TYPE_BIGPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
33
        Schema::TYPE_UBIGPK => 'integer UNSIGNED PRIMARY KEY AUTOINCREMENT NOT NULL',
34
        Schema::TYPE_CHAR => 'char(1)',
35
        Schema::TYPE_STRING => 'varchar(255)',
36
        Schema::TYPE_TEXT => 'text',
37
        Schema::TYPE_TINYINT => 'tinyint',
38
        Schema::TYPE_SMALLINT => 'smallint',
39
        Schema::TYPE_INTEGER => 'integer',
40
        Schema::TYPE_BIGINT => 'bigint',
41
        Schema::TYPE_FLOAT => 'float',
42
        Schema::TYPE_DOUBLE => 'double',
43
        Schema::TYPE_DECIMAL => 'decimal(10,0)',
44
        Schema::TYPE_DATETIME => 'datetime',
45
        Schema::TYPE_TIMESTAMP => 'timestamp',
46
        Schema::TYPE_TIME => 'time',
47
        Schema::TYPE_DATE => 'date',
48
        Schema::TYPE_BINARY => 'blob',
49
        Schema::TYPE_BOOLEAN => 'boolean',
50
        Schema::TYPE_MONEY => 'decimal(19,4)',
51
    ];
52
53
    /**
54
     * Contains array of default expression builders. Extend this method and override it, if you want to change default
55
     * expression builders for this query builder.
56
     *
57
     * @return array
58
     *
59
     * See {@see \Yiisoft\Db\Expression\ExpressionBuilder} docs for details.
60
     */
61 192
    protected function defaultExpressionBuilders(): array
62
    {
63 192
        return array_merge(parent::defaultExpressionBuilders(), [
64 192
            LikeCondition::class => LikeConditionBuilder::class,
65
            InCondition::class => InConditionBuilder::class,
66
        ]);
67
    }
68
69
    /**
70
     * Generates a batch INSERT SQL statement.
71
     *
72
     * For example,
73
     *
74
     * ```php
75
     * $connection->createCommand()->batchInsert('user', ['name', 'age'], [
76
     *     ['Tom', 30],
77
     *     ['Jane', 20],
78
     *     ['Linda', 25],
79
     * ])->execute();
80
     * ```
81
     *
82
     * Note that the values in each row must match the corresponding column names.
83
     *
84
     * @param string $table the table that new rows will be inserted into.
85
     * @param array $columns the column names
86
     * @param array|\Generator $rows the rows to be batch inserted into the table
87
     * @param array $params
88
     *
89
     * @throws Exception
90
     * @throws InvalidConfigException
91
     * @throws InvalidArgumentException
92
     * @throws NotSupportedException
93
     *
94
     * @return string the batch INSERT SQL statement.
95
     */
96 13
    public function batchInsert(string $table, array $columns, $rows, array &$params = []): string
97
    {
98 13
        if (empty($rows)) {
99 2
            return '';
100
        }
101
102
        /**
103
         * SQLite supports batch insert natively since 3.7.11.
104
         *
105
         * {@see http://www.sqlite.org/releaselog/3_7_11.html}
106
         */
107 12
        $this->db->open();
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

107
        $this->db->/** @scrutinizer ignore-call */ 
108
                   open();

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

180
        /** @scrutinizer ignore-call */ 
181
        $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...
181
182 1
        if ($table !== null && $table->getSequenceName() !== null) {
183 1
            $tableName = $db->quoteTableName($tableName);
184 1
            if ($value === null) {
185 1
                $pk = $table->getPrimaryKey();
186 1
                $key = $this->db->quoteColumnName(\reset($pk));
187
                $value = $this->db->useMaster(static function (Connection $db) use ($key, $tableName) {
188 1
                    return $db->createCommand("SELECT MAX($key) FROM $tableName")->queryScalar();
189 1
                });
190
            } else {
191 1
                $value = (int) $value - 1;
192
            }
193
194 1
            return "UPDATE sqlite_sequence SET seq='$value' WHERE name='{$table->getName()}'";
195
        } elseif ($table === null) {
196
            throw new InvalidArgumentException("Table not found: $tableName");
197
        }
198
199
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.'");
200
    }
201
202
    /**
203
     * Enables or disables integrity check.
204
     *
205
     * @param bool $check whether to turn on or off the integrity check.
206
     * @param string $schema the schema of the tables. Meaningless for SQLite.
207
     * @param string $table the table name. Meaningless for SQLite.
208
     *
209
     * @return string the SQL statement for checking integrity.
210
     */
211
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
212
    {
213
        return 'PRAGMA foreign_keys=' . (int) $check;
214
    }
215
216
    /**
217
     * Builds a SQL statement for truncating a DB table.
218
     *
219
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
220
     *
221
     * @throws Exception
222
     * @throws InvalidConfigException
223
     * @throws NotSupportedException
224
     *
225
     * @return string the SQL statement for truncating a DB table.
226
     */
227 1
    public function truncateTable(string $table): string
228
    {
229 1
        return 'DELETE FROM ' . $this->db->quoteTableName($table);
230
    }
231
232
    /**
233
     * Builds a SQL statement for dropping an index.
234
     *
235
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
236
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
237
     *
238
     * @throws Exception
239
     * @throws InvalidConfigException
240
     * @throws NotSupportedException
241
     *
242
     * @return string the SQL statement for dropping an index.
243
     */
244 2
    public function dropIndex(string $name, string $table): string
245
    {
246 2
        return 'DROP INDEX ' . $this->db->quoteTableName($name);
247
    }
248
249
    /**
250
     * Builds a SQL statement for dropping a DB column.
251
     *
252
     * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
253
     * @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
254
     *
255
     * @throws NotSupportedException this is not supported by SQLite.
256
     *
257
     * @return string the SQL statement for dropping a DB column.
258
     */
259
    public function dropColumn(string $table, string $column): string
260
    {
261
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
262
    }
263
264
    /**
265
     * Builds a SQL statement for renaming a column.
266
     *
267
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
268
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
269
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
270
     *
271
     * @throws NotSupportedException this is not supported by SQLite.
272
     *
273
     * @return string the SQL statement for renaming a DB column.
274
     */
275
    public function renameColumn(string $table, string $oldName, string $newName): string
276
    {
277
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
278
    }
279
280
    /**
281
     * Builds a SQL statement for adding a foreign key constraint to an existing table.
282
     *
283
     * The method will properly quote the table and column names.
284
     *
285
     * @param string $name the name of the foreign key constraint.
286
     * @param string $table the table that the foreign key constraint will be added to.
287
     * @param string|array $columns the name of the column to that the constraint will be added on. If there are
288
     * multiple columns, separate them with commas or use an array to represent them.
289
     * @param string $refTable the table that the foreign key references to.
290
     * @param string|array $refColumns the name of the column that the foreign key references to. If there are multiple
291
     * columns, separate them with commas or use an array to represent them.
292
     * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
293
     * SET DEFAULT, SET NULL.
294
     * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
295
     * SET DEFAULT, SET NULL.
296
     *
297
     * @throws NotSupportedException this is not supported by SQLite.
298
     *
299
     * @return string the SQL statement for adding a foreign key constraint to an existing table.
300
     */
301
    public function addForeignKey(
302
        string $name,
303
        string $table,
304
        $columns,
305
        string $refTable,
306
        $refColumns,
307
        ?string $delete = null,
308
        ?string $update = null
309
    ): string {
310
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
311
    }
312
313
    /**
314
     * Builds a SQL statement for dropping a foreign key constraint.
315
     *
316
     * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted
317
     * by the method.
318
     * @param string $table the table whose foreign is to be dropped. The name will be properly quoted by the method.
319
     *
320
     * @throws NotSupportedException this is not supported by SQLite.
321
     *
322
     * @return string the SQL statement for dropping a foreign key constraint.
323
     */
324
    public function dropForeignKey(string $name, string $table): string
325
    {
326
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
327
    }
328
329
    /**
330
     * Builds a SQL statement for renaming a DB table.
331
     *
332
     * @param string $table the table to be renamed. The name will be properly quoted by the method.
333
     * @param string $newName the new table name. The name will be properly quoted by the method.
334
     *
335
     * @throws Exception
336
     * @throws InvalidConfigException
337
     * @throws NotSupportedException
338
     *
339
     * @return string the SQL statement for renaming a DB table.
340
     */
341 3
    public function renameTable(string $table, string $newName): string
342
    {
343 3
        return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' RENAME TO ' . $this->db->quoteTableName($newName);
344
    }
345
346
    /**
347
     * Builds a SQL statement for changing the definition of a column.
348
     *
349
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the
350
     * method.
351
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
352
     * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract
353
     * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept in the
354
     * generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become
355
     * 'varchar(255) not null'.
356
     *
357
     * @throws NotSupportedException this is not supported by SQLite.
358
     *
359
     * @return string the SQL statement for changing the definition of a column.
360
     */
361
    public function alterColumn(string $table, string $column, string $type): string
362
    {
363
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
364
    }
365
366
    /**
367
     * Builds a SQL statement for adding a primary key constraint to an existing table.
368
     *
369
     * @param string $name the name of the primary key constraint.
370
     * @param string $table the table that the primary key constraint will be added to.
371
     * @param string|array $columns comma separated string or array of columns that the primary key will consist of.
372
     *
373
     * @throws NotSupportedException this is not supported by SQLite.
374
     *
375
     * @return string the SQL statement for adding a primary key constraint to an existing table.
376
     */
377
    public function addPrimaryKey(string $name, string $table, $columns): string
378
    {
379
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
380
    }
381
382
    /**
383
     * Builds a SQL statement for removing a primary key constraint to an existing table.
384
     *
385
     * @param string $name the name of the primary key constraint to be removed.
386
     * @param string $table the table that the primary key constraint will be removed from.
387
     *
388
     * @throws NotSupportedException this is not supported by SQLite.
389
     *
390
     * @return string the SQL statement for removing a primary key constraint from an existing table.
391
     */
392
    public function dropPrimaryKey(string $name, string $table): string
393
    {
394
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
395
    }
396
397
    /**
398
     * Creates a SQL command for adding an unique constraint to an existing table.
399
     *
400
     * @param string $name the name of the unique constraint. The name will be properly quoted by the method.
401
     * @param string $table the table that the unique constraint will be added to. The name will be properly quoted by
402
     * the method.
403
     * @param string|array $columns the name of the column to that the constraint will be added on. If there are
404
     * multiple columns, separate them with commas. The name will be properly quoted by the method.
405
     *
406
     * @throws Exception
407
     * @throws NotSupportedException
408
     *
409
     * @return string the SQL statement for adding an unique constraint to an existing table.
410
     */
411
    public function addUnique(string $name, string $table, $columns): string
412
    {
413
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
414
    }
415
416
    /**
417
     * Creates a SQL command for dropping an unique constraint.
418
     *
419
     * @param string $name the name of the unique constraint to be dropped. The name will be properly quoted by the
420
     * method.
421
     * @param string $table the table whose unique constraint is to be dropped. The name will be properly quoted by the
422
     * method.
423
     *
424
     * @throws Exception
425
     * @throws NotSupportedException
426
     *
427
     * @return string the SQL statement for dropping an unique constraint.
428
     */
429
    public function dropUnique(string $name, string $table): string
430
    {
431
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
432
    }
433
434
    /**
435
     * Creates a SQL command for adding a check constraint to an existing table.
436
     *
437
     * @param string $name the name of the check constraint. The name will be properly quoted by the method.
438
     * @param string $table the table that the check constraint will be added to. The name will be properly quoted by
439
     * the method.
440
     * @param string $expression the SQL of the `CHECK` constraint.
441
     *
442
     * @throws Exception
443
     * @throws NotSupportedException
444
     *
445
     * @return string the SQL statement for adding a check constraint to an existing table.
446
     */
447
    public function addCheck(string $name, string $table, string $expression): string
448
    {
449
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
450
    }
451
452
    /**
453
     * Creates a SQL command for dropping a check constraint.
454
     *
455
     * @param string $name the name of the check constraint to be dropped. The name will be properly quoted by the
456
     * method.
457
     * @param string $table the table whose check constraint is to be dropped. The name will be properly quoted by the
458
     * method.
459
     *
460
     * @throws Exception
461
     * @throws NotSupportedException
462
     *
463
     * @return string the SQL statement for dropping a check constraint.
464
     */
465
    public function dropCheck(string $name, string $table): string
466
    {
467
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
468
    }
469
470
    /**
471
     * Creates a SQL command for adding a default value constraint to an existing table.
472
     *
473
     * @param string $name the name of the default value constraint. The name will be properly quoted by the method.
474
     * @param string $table the table that the default value constraint will be added to. The name will be properly
475
     * quoted by the method.
476
     * @param string $column the name of the column to that the constraint will be added on. The name will be properly
477
     * quoted by the method.
478
     * @param mixed $value default value.
479
     *
480
     * @throws Exception
481
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
482
     *
483
     * @return string the SQL statement for adding a default value constraint to an existing table.
484
     */
485
    public function addDefaultValue(string $name, string $table, string $column, $value): string
486
    {
487
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
488
    }
489
490
    /**
491
     * Creates a SQL command for dropping a default value constraint.
492
     *
493
     * @param string $name the name of the default value constraint to be dropped. The name will be properly quoted by
494
     * the method.
495
     * @param string $table the table whose default value constraint is to be dropped. The name will be properly quoted
496
     * by the method.
497
     *
498
     * @throws Exception
499
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
500
     *
501
     * @return string the SQL statement for dropping a default value constraint.
502
     */
503
    public function dropDefaultValue(string $name, string $table): string
504
    {
505
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
506
    }
507
508
    /**
509
     * Builds a SQL command for adding comment to column.
510
     *
511
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
512
     * method.
513
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
514
     * method.
515
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
516
     *
517
     * @throws Exception
518
     * @throws NotSupportedException
519
     *
520
     * @return string the SQL statement for adding comment on column.
521
     */
522
    public function addCommentOnColumn(string $table, string $column, string $comment): string
523
    {
524
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
525
    }
526
527
    /**
528
     * Builds a SQL command for adding comment to table.
529
     *
530
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
531
     * method.
532
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
533
     *
534
     * @throws Exception
535
     * @throws NotSupportedException
536
     *
537
     * @return string the SQL statement for adding comment on table.
538
     */
539
    public function addCommentOnTable(string $table, string $comment): string
540
    {
541
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
542
    }
543
544
    /**
545
     * Builds a SQL command for adding comment to column.
546
     *
547
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
548
     * method.
549
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
550
     * method.
551
     *
552
     * @throws Exception
553
     * @throws NotSupportedException
554
     *
555
     * @return string the SQL statement for adding comment on column.
556
     */
557
    public function dropCommentFromColumn(string $table, string $column): string
558
    {
559
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
560
    }
561
562
    /**
563
     * Builds a SQL command for adding comment to table.
564
     *
565
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
566
     * method.
567
     *
568
     * @throws Exception
569
     * @throws NotSupportedException
570
     *
571
     * @return string the SQL statement for adding comment on column.
572
     */
573
    public function dropCommentFromTable(string $table): string
574
    {
575
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
576
    }
577
578
    /**
579
     * @param int|object|null $limit
580
     * @param int|object|null $offset
581
     *
582
     * @return string the LIMIT and OFFSET clauses.
583
     */
584 136
    public function buildLimit($limit, $offset): string
585
    {
586 136
        $sql = '';
587
588 136
        if ($this->hasLimit($limit)) {
589 8
            $sql = 'LIMIT ' . $limit;
0 ignored issues
show
Bug introduced by
Are you sure $limit of type integer|null|object can be used in concatenation? ( Ignorable by Annotation )

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

589
            $sql = 'LIMIT ' . /** @scrutinizer ignore-type */ $limit;
Loading history...
590 8
            if ($this->hasOffset($offset)) {
591 8
                $sql .= ' OFFSET ' . $offset;
0 ignored issues
show
Bug introduced by
Are you sure $offset of type integer|null|object can be used in concatenation? ( Ignorable by Annotation )

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

591
                $sql .= ' OFFSET ' . /** @scrutinizer ignore-type */ $offset;
Loading history...
592
            }
593 131
        } elseif ($this->hasOffset($offset)) {
594
            /**
595
             * limit is not optional in SQLite.
596
             *
597
             * {@see http://www.sqlite.org/syntaxdiagrams.html#select-stmt}
598
             */
599
            $sql = "LIMIT 9223372036854775807 OFFSET $offset"; // 2^63-1
600
        }
601
602 136
        return $sql;
603
    }
604
605
    /**
606
     * Generates a SELECT SQL statement from a {@see Query} object.
607
     *
608
     * @param Query $query the {@see Query} object from which the SQL statement will be generated.
609
     * @param array $params the parameters to be bound to the generated SQL statement. These parameters will be included
610
     * in the result with the additional parameters generated during the query building process.
611
     *
612
     * @throws Exception
613
     * @throws InvalidArgumentException
614
     * @throws InvalidConfigException
615
     * @throws NotSupportedException
616
     *
617
     * @return array the generated SQL statement (the first array element) and the corresponding parameters to be bound
618
     * to the SQL statement (the second array element). The parameters returned include those provided in `$params`.
619
     */
620 136
    public function build(Query $query, array $params = []): array
621
    {
622 136
        $query = $query->prepare($this);
623
624 136
        $params = empty($params) ? $query->getParams() : \array_merge($params, $query->getParams());
625
626
        $clauses = [
627 136
            $this->buildSelect($query->getSelect(), $params, $query->getDistinct(), $query->getSelectOption()),
628 136
            $this->buildFrom($query->getFrom(), $params),
629 136
            $this->buildJoin($query->getJoin(), $params),
630 136
            $this->buildWhere($query->getWhere(), $params),
631 136
            $this->buildGroupBy($query->getGroupBy()),
632 136
            $this->buildHaving($query->getHaving(), $params),
633
        ];
634
635 136
        $sql = \implode($this->separator, \array_filter($clauses));
636 136
        $sql = $this->buildOrderByAndLimit($sql, $query->getOrderBy(), $query->getLimit(), $query->getOffset());
637
638 136
        if (!empty($query->getOrderBy())) {
639 5
            foreach ($query->getOrderBy() as $expression) {
640 5
                if ($expression instanceof ExpressionInterface) {
641 1
                    $this->buildExpression($expression, $params);
642
                }
643
            }
644
        }
645
646 136
        if (!empty($query->getGroupBy())) {
647 2
            foreach ($query->getGroupBy() as $expression) {
648 2
                if ($expression instanceof ExpressionInterface) {
649 1
                    $this->buildExpression($expression, $params);
650
                }
651
            }
652
        }
653
654 136
        $union = $this->buildUnion($query->getUnion(), $params);
655
656 136
        if ($union !== '') {
657 3
            $sql = "$sql{$this->separator}$union";
658
        }
659
660 136
        $with = $this->buildWithQueries($query->getWithQueries(), $params);
661
662 136
        if ($with !== '') {
663 2
            $sql = "$with{$this->separator}$sql";
664
        }
665
666 136
        return [$sql, $params];
667
    }
668
669
    /**
670
     * Builds a SQL statement for creating a new index.
671
     *
672
     * @param string $name the name of the index. The name will be properly quoted by the method.
673
     * @param string $table the table that the new index will be created for. The table name will be properly quoted by
674
     * the method.
675
     * @param string|array $columns the column(s) that should be included in the index. If there are multiple columns,
676
     * separate them with commas or use an array to represent them. Each column name will be properly quoted by the
677
     * method, unless a parenthesis is found in the name.
678
     * @param bool $unique whether to add UNIQUE constraint on the created index.
679
     *
680
     * @throws Exception
681
     * @throws InvalidArgumentException
682
     * @throws InvalidConfigException
683
     * @throws NotSupportedException
684
     *
685
     * @return string the SQL statement for creating a new index.
686
     */
687 6
    public function createIndex(string $name, string $table, $columns, bool $unique = false): string
688
    {
689 6
        $sql = ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ')
690 6
            . $this->db->quoteTableName($name) . ' ON '
691 6
            . $this->db->quoteTableName($table)
692 6
            . ' (' . $this->buildColumns($columns) . ')';
693
694 6
        $sql = \preg_replace_callback(
695 6
            '/(`.*`) ON ({{(%?)([\w\-]+)}\}\.{{((%?)[\w\-]+)\\}\\})|(`.*`) ON ({{(%?)([\w\-]+)\.([\w\-]+)\\}\\})/',
696
            static function ($matches) {
697
                if (!empty($matches[1])) {
698
                    return $matches[4] . "." . $matches[1]
699
                     . ' ON {{' . $matches[3] . $matches[5] . '}}';
700
                }
701
702
                if (!empty($matches[7])) {
703
                    return $matches[10] . '.' . $matches[7]
704
                     . ' ON {{' . $matches[9] . $matches[11] . '}}';
705
                }
706 6
            },
707 6
            $sql
708
        );
709
710 6
        return $sql;
711
    }
712
713
    /**
714
     * @param array $unions
715
     * @param array $params the binding parameters to be populated.
716
     *
717
     * @throws Exception
718
     * @throws InvalidArgumentException
719
     * @throws InvalidConfigException
720
     * @throws NotSupportedException
721
     *
722
     * @return string the UNION clause built from {@see Query::$union}.
723
     */
724 136
    public function buildUnion(array $unions, array &$params = []): string
725
    {
726 136
        if (empty($unions)) {
727 136
            return '';
728
        }
729
730 3
        $result = '';
731
732 3
        foreach ($unions as $i => $union) {
733 3
            $query = $union['query'];
734 3
            if ($query instanceof Query) {
735 3
                [$unions[$i]['query'], $params] = $this->build($query, $params);
736
            }
737
738 3
            $result .= ' UNION ' . ($union['all'] ? 'ALL ' : '') . ' ' . $unions[$i]['query'];
739
        }
740
741 3
        return \trim($result);
742
    }
743
744
    /**
745
     * Creates an SQL statement to insert rows into a database table if they do not already exist (matching unique
746
     * constraints), or update them if they do.
747
     *
748
     * For example,
749
     *
750
     * ```php
751
     * $sql = $queryBuilder->upsert('pages', [
752
     *     'name' => 'Front page',
753
     *     'url' => 'http://example.com/', // url is unique
754
     *     'visits' => 0,
755
     * ], [
756
     *     'visits' => new \Yiisoft\Db\Expression('visits + 1'),
757
     * ], $params);
758
     * ```
759
     *
760
     * The method will properly escape the table and column names.
761
     *
762
     * @param string $table the table that new rows will be inserted into/updated in.
763
     * @param array|Query $insertColumns the column data (name => value) to be inserted into the table or instance
764
     * of {@see Query} to perform `INSERT INTO ... SELECT` SQL statement.
765
     * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist.
766
     * If `true` is passed, the column data will be updated to match the insert column data.
767
     * If `false` is passed, no update will be performed if the column data already exists.
768
     * @param array $params the binding parameters that will be generated by this method.
769
     * They should be bound to the DB command later.
770
     *
771
     * @throws Exception
772
     * @throws InvalidConfigException
773
     * @throws NotSupportedException if this is not supported by the underlying DBMS.
774
     *
775
     * @return string the resulting SQL.
776
     */
777 18
    public function upsert(string $table, $insertColumns, $updateColumns, array &$params): string
778
    {
779
        /** @var Constraint[] $constraints */
780 18
        $constraints = [];
781
782 18
        [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns(
783 18
            $table,
784
            $insertColumns,
785
            $updateColumns,
786
            $constraints
787
        );
788
789 18
        if (empty($uniqueNames)) {
790 3
            return $this->insert($table, $insertColumns, $params);
791
        }
792
793 15
        [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);
794
795 15
        $insertSql = 'INSERT OR IGNORE INTO ' . $this->db->quoteTableName($table)
796 15
            . (!empty($insertNames) ? ' (' . \implode(', ', $insertNames) . ')' : '')
797 15
            . (!empty($placeholders) ? ' VALUES (' . \implode(', ', $placeholders) . ')' : $values);
798
799 15
        if ($updateColumns === false) {
800 5
            return $insertSql;
801
        }
802
803 10
        $updateCondition = ['or'];
804 10
        $quotedTableName = $this->db->quoteTableName($table);
805
806 10
        foreach ($constraints as $constraint) {
807 10
            $constraintCondition = ['and'];
808 10
            foreach ($constraint->getColumnNames() as $name) {
809 10
                $quotedName = $this->db->quoteColumnName($name);
810 10
                $constraintCondition[] = "$quotedTableName.$quotedName=(SELECT $quotedName FROM `EXCLUDED`)";
811
            }
812 10
            $updateCondition[] = $constraintCondition;
813
        }
814
815 10
        if ($updateColumns === true) {
816 4
            $updateColumns = [];
817 4
            foreach ($updateNames as $name) {
818 4
                $quotedName = $this->db->quoteColumnName($name);
819
820 4
                if (\strrpos($quotedName, '.') === false) {
821 4
                    $quotedName = "(SELECT $quotedName FROM `EXCLUDED`)";
822
                }
823 4
                $updateColumns[$name] = new Expression($quotedName);
824
            }
825
        }
826
827 10
        $updateSql = 'WITH "EXCLUDED" (' . \implode(', ', $insertNames)
828 10
            . ') AS (' . (!empty($placeholders) ? 'VALUES (' . \implode(', ', $placeholders) . ')'
829 10
            : \ltrim($values, ' ')) . ') ' . $this->update($table, $updateColumns, $updateCondition, $params);
830
831 10
        return "$updateSql; $insertSql;";
832
    }
833
}
834