Passed
Push — master ( b1a1af...f07e26 )
by Alexander
25:59 queued 19:47
created

QueryBuilder::dropIndex()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 1
c 1
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 2
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Db\Sqlite;
6
7
use Generator;
8
use JsonException;
9
use Throwable;
10
use Yiisoft\Db\Constraint\Constraint;
11
use Yiisoft\Db\Exception\Exception;
12
use Yiisoft\Db\Exception\InvalidArgumentException;
13
use Yiisoft\Db\Exception\InvalidConfigException;
14
use Yiisoft\Db\Exception\InvalidParamException;
15
use Yiisoft\Db\Exception\NotSupportedException;
16
use Yiisoft\Db\Expression\Expression;
17
use Yiisoft\Db\Expression\ExpressionBuilder;
18
use Yiisoft\Db\Expression\ExpressionInterface;
19
use Yiisoft\Db\Query\Conditions\InCondition;
20
use Yiisoft\Db\Query\Conditions\LikeCondition;
21
use Yiisoft\Db\Query\Query;
22
use Yiisoft\Db\Query\QueryBuilder as BaseQueryBuilder;
23
use Yiisoft\Db\Sqlite\Condition\InConditionBuilder;
24
use Yiisoft\Db\Sqlite\Condition\LikeConditionBuilder;
25
use Yiisoft\Strings\NumericHelper;
26
27
use function array_column;
28
use function array_filter;
29
use function array_merge;
30
use function implode;
31
use function is_float;
32
use function is_string;
33
use function ltrim;
34
use function reset;
35
use function strpos;
36
use function strrpos;
37
use function substr;
38
use function trim;
39
use function version_compare;
40
41
final class QueryBuilder extends BaseQueryBuilder
42
{
43
    /**
44
     * @var array mapping from abstract column types (keys) to physical column types (values).
45
     */
46
    protected array $typeMap = [
47
        Schema::TYPE_PK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
48
        Schema::TYPE_UPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
49
        Schema::TYPE_BIGPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
50
        Schema::TYPE_UBIGPK => 'integer PRIMARY KEY AUTOINCREMENT NOT NULL',
51
        Schema::TYPE_CHAR => 'char(1)',
52
        Schema::TYPE_STRING => 'varchar(255)',
53
        Schema::TYPE_TEXT => 'text',
54
        Schema::TYPE_TINYINT => 'tinyint',
55
        Schema::TYPE_SMALLINT => 'smallint',
56
        Schema::TYPE_INTEGER => 'integer',
57
        Schema::TYPE_BIGINT => 'bigint',
58
        Schema::TYPE_FLOAT => 'float',
59
        Schema::TYPE_DOUBLE => 'double',
60
        Schema::TYPE_DECIMAL => 'decimal(10,0)',
61
        Schema::TYPE_DATETIME => 'datetime',
62
        Schema::TYPE_TIMESTAMP => 'timestamp',
63
        Schema::TYPE_TIME => 'time',
64
        Schema::TYPE_DATE => 'date',
65
        Schema::TYPE_BINARY => 'blob',
66
        Schema::TYPE_BOOLEAN => 'boolean',
67
        Schema::TYPE_MONEY => 'decimal(19,4)',
68
    ];
69
70
    /**
71
     * Contains array of default expression builders. Extend this method and override it, if you want to change default
72
     * expression builders for this query builder.
73
     *
74
     * @return array
75
     *
76
     * See {@see ExpressionBuilder} docs for details.
77
     */
78 236
    protected function defaultExpressionBuilders(): array
79
    {
80 236
        return array_merge(parent::defaultExpressionBuilders(), [
81 236
            LikeCondition::class => LikeConditionBuilder::class,
82
            InCondition::class => InConditionBuilder::class,
83
        ]);
84
    }
85
86
    /**
87
     * Generates a batch INSERT SQL statement.
88
     *
89
     * For example,
90
     *
91
     * ```php
92
     * $connection->createCommand()->batchInsert('user', ['name', 'age'], [
93
     *     ['Tom', 30],
94
     *     ['Jane', 20],
95
     *     ['Linda', 25],
96
     * ])->execute();
97
     * ```
98
     *
99
     * Note that the values in each row must match the corresponding column names.
100
     *
101
     * @param string $table the table that new rows will be inserted into.
102
     * @param array $columns the column names
103
     * @param array|Generator $rows the rows to be batch inserted into the table
104
     * @param array $params
105
     *
106
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
107
     *
108
     * @return string the batch INSERT SQL statement.
109
     */
110 13
    public function batchInsert(string $table, array $columns, $rows, array &$params = []): string
111
    {
112 13
        if (empty($rows)) {
113 2
            return '';
114
        }
115
116
        /**
117
         * SQLite supports batch insert natively since 3.7.11.
118
         *
119
         * {@see http://www.sqlite.org/releaselog/3_7_11.html}
120
         */
121 12
        $this->getDb()->open();
122
123 12
        if (version_compare($this->getDb()->getServerVersion(), '3.7.11', '>=')) {
124 12
            return parent::batchInsert($table, $columns, $rows, $params);
125
        }
126
127
        $schema = $this->getDb()->getSchema();
128
129
        if (($tableSchema = $schema->getTableSchema($table)) !== null) {
130
            $columnSchemas = $tableSchema->getColumns();
131
        } else {
132
            $columnSchemas = [];
133
        }
134
135
        $values = [];
136
137
        foreach ($rows as $row) {
138
            $vs = [];
139
            foreach ($row as $i => $value) {
140
                if (isset($columnSchemas[$columns[$i]])) {
141
                    $value = $columnSchemas[$columns[$i]]->dbTypecast($value);
142
                }
143
                if (is_string($value)) {
144
                    $value = $schema->quoteValue($value);
145
                } elseif (is_float($value)) {
146
                    /** ensure type cast always has . as decimal separator in all locales */
147
                    $value = NumericHelper::normalize($value);
148
                } elseif ($value === false) {
149
                    $value = 0;
150
                } elseif ($value === null) {
151
                    $value = 'NULL';
152
                } elseif ($value instanceof ExpressionInterface) {
153
                    $value = $this->buildExpression($value, $params);
154
                }
155
                $vs[] = $value;
156
            }
157
            $values[] = implode(', ', $vs);
158
        }
159
160
        if (empty($values)) {
161
            return '';
162
        }
163
164
        foreach ($columns as $i => $name) {
165
            $columns[$i] = $schema->quoteColumnName($name);
166
        }
167
168
        return 'INSERT INTO ' . $schema->quoteTableName($table)
169
        . ' (' . implode(', ', $columns) . ') SELECT ' . implode(' UNION SELECT ', $values);
170
    }
171
172
    /**
173
     * Creates a SQL statement for resetting the sequence value of a table's primary key.
174
     *
175
     * The sequence will be reset such that the primary key of the next new row inserted will have the specified value
176
     * or 1.
177
     *
178
     * @param string $tableName the name of the table whose primary key sequence will be reset.
179
     * @param mixed $value the value for the primary key of the next new row inserted. If this is not set, the next new
180
     * row's primary key will have a value 1.
181
     *
182
     * @throws Exception|InvalidArgumentException|Throwable if the table does not exist or there is no sequence
183
     * associated with the table.
184
     *
185
     * @return string the SQL statement for resetting sequence.
186
     */
187 1
    public function resetSequence(string $tableName, $value = null): string
188
    {
189 1
        $db = $this->getDb();
190
191 1
        $table = $db->getTableSchema($tableName);
192
193 1
        if ($table !== null && $table->getSequenceName() !== null) {
194 1
            $tableName = $db->quoteTableName($tableName);
195 1
            if ($value === null) {
196 1
                $pk = $table->getPrimaryKey();
197 1
                $key = $this->getDb()->quoteColumnName(reset($pk));
198 1
                $value = $this->getDb()->useMaster(static function (Connection $db) use ($key, $tableName) {
199 1
                    return $db->createCommand("SELECT MAX($key) FROM $tableName")->queryScalar();
200 1
                });
201
            } else {
202 1
                $value = (int) $value - 1;
203
            }
204
205 1
            return "UPDATE sqlite_sequence SET seq='$value' WHERE name='{$table->getName()}'";
206
        }
207
208
        if ($table === null) {
209
            throw new InvalidArgumentException("Table not found: $tableName");
210
        }
211
212
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.'");
213
    }
214
215
    /**
216
     * Enables or disables integrity check.
217
     *
218
     * @param bool $check whether to turn on or off the integrity check.
219
     * @param string $schema the schema of the tables. Meaningless for SQLite.
220
     * @param string $table the table name. Meaningless for SQLite.
221
     *
222
     * @return string the SQL statement for checking integrity.
223
     */
224
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
225
    {
226
        return 'PRAGMA foreign_keys=' . (int) $check;
227
    }
228
229
    /**
230
     * Builds a SQL statement for truncating a DB table.
231
     *
232
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
233
     *
234
     * @return string the SQL statement for truncating a DB table.
235
     */
236 1
    public function truncateTable(string $table): string
237
    {
238 1
        return 'DELETE FROM ' . $this->getDb()->quoteTableName($table);
239
    }
240
241
    /**
242
     * Builds a SQL statement for dropping an index.
243
     *
244
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
245
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
246
     *
247
     * @return string the SQL statement for dropping an index.
248
     */
249 2
    public function dropIndex(string $name, string $table): string
250
    {
251 2
        return 'DROP INDEX ' . $this->getDb()->quoteTableName($name);
252
    }
253
254
    /**
255
     * Builds a SQL statement for dropping a DB column.
256
     *
257
     * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
258
     * @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
259
     *
260
     * @throws NotSupportedException this is not supported by SQLite.
261
     *
262
     * @return string the SQL statement for dropping a DB column.
263
     */
264
    public function dropColumn(string $table, string $column): string
265
    {
266
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
267
    }
268
269
    /**
270
     * Builds a SQL statement for renaming a column.
271
     *
272
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
273
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
274
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
275
     *
276
     * @throws NotSupportedException this is not supported by SQLite.
277
     *
278
     * @return string the SQL statement for renaming a DB column.
279
     */
280
    public function renameColumn(string $table, string $oldName, string $newName): string
281
    {
282
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
283
    }
284
285
    /**
286
     * Builds a SQL statement for adding a foreign key constraint to an existing table.
287
     *
288
     * The method will properly quote the table and column names.
289
     *
290
     * @param string $name the name of the foreign key constraint.
291
     * @param string $table the table that the foreign key constraint will be added to.
292
     * @param array|string $columns the name of the column to that the constraint will be added on. If there are
293
     * multiple columns, separate them with commas or use an array to represent them.
294
     * @param string $refTable the table that the foreign key references to.
295
     * @param array|string $refColumns the name of the column that the foreign key references to. If there are multiple
296
     * columns, separate them with commas or use an array to represent them.
297
     * @param string|null $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
298
     * SET DEFAULT, SET NULL.
299
     * @param string|null $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
300
     * SET DEFAULT, SET NULL.
301
     *
302
     * @throws Exception|InvalidParamException
303
     *
304
     * @return string the SQL statement for adding a foreign key constraint to an existing table.
305
     */
306 4
    public function addForeignKey(
307
        string $name,
308
        string $table,
309
        $columns,
310
        string $refTable,
311
        $refColumns,
312
        ?string $delete = null,
313
        ?string $update = null
314
    ): string {
315 4
        $schema = $refschema = '';
316
317 4
        if (($pos = strpos($table, '.')) !== false) {
318
            $schema = $this->unquoteTableName(substr($table, 0, $pos));
319
            $table = substr($table, $pos + 1);
320
        }
321
322 4
        if (($pos_ref = strpos($refTable, '.')) !== false) {
323
            $refschema = substr($refTable, 0, $pos_ref);
324
            $refTable = substr($refTable, $pos_ref + 1);
325
        }
326
327 4
        if (($schema !== '' || ($refschema !== '' && $schema !== $refschema))) {
328
            return '' ;
329
        }
330
331
        /** @psalm-suppress TypeDoesNotContainType */
332 4
        if ($schema !== '') {
333
            $tmp_table_name = "temp_{$schema}_" . $this->unquoteTableName($table);
334
            $schema .= '.';
335
            $unquoted_tablename = $schema . $this->unquoteTableName($table);
336
            $quoted_tablename = $schema . $this->getDb()->quoteTableName($table);
337
        } else {
338 4
            $unquoted_tablename = $this->unquoteTableName($table);
339 4
            $quoted_tablename = $this->getDb()->quoteTableName($table);
340 4
            $tmp_table_name = 'temp_' . $this->unquoteTableName($table);
341
        }
342
343 4
        $fields_definitions_tokens = $this->getFieldDefinitionsTokens($unquoted_tablename);
344 4
        $ddl_fields_defs = $fields_definitions_tokens->getSql();
345 4
        $ddl_fields_defs .= ",\nCONSTRAINT " . $this->getDb()->quoteColumnName($name) . ' FOREIGN KEY (' .
346 4
            implode(',', (array)$columns) . ") REFERENCES $refTable(" . implode(',', (array)$refColumns) . ')';
347
348 4
        if ($update !== null) {
349 2
            $ddl_fields_defs .= " ON UPDATE $update";
350
        }
351
352 4
        if ($delete !== null) {
353 2
            $ddl_fields_defs .= " ON DELETE $delete";
354
        }
355
356 4
        $foreign_keys_state = $this->foreignKeysState();
357 4
        $return_queries = [];
358 4
        $return_queries[] = 'PRAGMA foreign_keys = off';
359 4
        $return_queries[] = "SAVEPOINT add_foreign_key_to_$tmp_table_name";
360 4
        $return_queries[] = 'CREATE TEMP TABLE ' . $this->getDb()->quoteTableName($tmp_table_name)
361 4
            . " AS SELECT * FROM $quoted_tablename";
362 4
        $return_queries[] = "DROP TABLE $quoted_tablename";
363 4
        $return_queries[] = "CREATE TABLE $quoted_tablename (" . trim($ddl_fields_defs, " \n\r\t,") . ')';
364 4
        $return_queries[] = "INSERT INTO $quoted_tablename SELECT * FROM " . $this->getDb()->quoteTableName($tmp_table_name);
365 4
        $return_queries[] = 'DROP TABLE ' . $this->getDb()->quoteTableName($tmp_table_name);
366 4
        $return_queries = array_merge($return_queries, $this->getIndexSqls($unquoted_tablename));
367
368 4
        $return_queries[] = "RELEASE add_foreign_key_to_$tmp_table_name";
369 4
        $return_queries[] = "PRAGMA foreign_keys = $foreign_keys_state";
370
371 4
        return implode(';', $return_queries);
372
    }
373
374
    /**
375
     * Builds a SQL statement for dropping a foreign key constraint.
376
     *
377
     * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted
378
     * by the method.
379
     * @param string $table
380
     *
381
     * @throws Exception|InvalidParamException|NotSupportedException
382
     *
383
     * @return string the SQL statement for dropping a foreign key constraint.
384
     */
385 2
    public function dropForeignKey(string $name, string $table): string
386
    {
387 2
        $return_queries = [];
388 2
        $ddl_fields_def = '';
389 2
        $sql_fields_to_insert = [];
390 2
        $skipping = false;
391 2
        $foreign_found = false;
392 2
        $quoted_foreign_name = $this->getDb()->quoteColumnName($name);
393
394 2
        $quoted_tablename = $this->getDb()->quoteTableName($table);
395 2
        $unquoted_tablename = $this->unquoteTableName($table);
396
397 2
        $fields_definitions_tokens = $this->getFieldDefinitionsTokens($unquoted_tablename);
398
399 2
        $offset = 0;
400 2
        $constraint_pos = 0;
401
402
        /** Traverse the tokens looking for either an identifier (field name) or a foreign key */
403 2
        while ($fields_definitions_tokens->offsetExists($offset)) {
404 2
            $token = $fields_definitions_tokens[$offset++];
405
406
            /**
407
             * These searchs could be done with another SqlTokenizer, but I don't konw how to do them, the documentation
408
             * for sqltokenizer si really scarse.
409
             */
410 2
            $tokenType = $token->getType();
0 ignored issues
show
Bug introduced by
The method getType() 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

410
            /** @scrutinizer ignore-call */ 
411
            $tokenType = $token->getType();

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...
411
412 2
            if ($tokenType === SqlToken::TYPE_IDENTIFIER) {
413 2
                $identifier = (string) $token;
414 2
                $sql_fields_to_insert[] = $identifier;
415 2
            } elseif ($tokenType === SqlToken::TYPE_KEYWORD) {
416 2
                $keyword = (string) $token;
417
418 2
                if ($keyword === 'CONSTRAINT' || $keyword === 'FOREIGN') {
419
                    /** Constraint key found */
420 2
                    $other_offset = $offset;
421
422 2
                    if ($keyword === 'CONSTRAINT') {
423 2
                        $constraint_name = $this->getDb()->quoteColumnName(
424 2
                            $fields_definitions_tokens[$other_offset]->getContent()
0 ignored issues
show
Bug introduced by
The method getContent() 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

424
                            $fields_definitions_tokens[$other_offset]->/** @scrutinizer ignore-call */ 
425
                                                                       getContent()

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...
Bug introduced by
It seems like $fields_definitions_toke...r_offset]->getContent() can also be of type null; however, parameter $name of Yiisoft\Db\Connection\Co...tion::quoteColumnName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

424
                            /** @scrutinizer ignore-type */ $fields_definitions_tokens[$other_offset]->getContent()
Loading history...
425
                        );
426
                    } else {
427
                        $constraint_name = $this->getDb()->quoteColumnName((string) $constraint_pos);
428
                    }
429
430
                    /** @psalm-suppress TypeDoesNotContainType */
431 2
                    if (($constraint_name === $quoted_foreign_name) || (is_int($name) && $constraint_pos === $name)) {
432
                        /** Found foreign key $name, skip it */
433 2
                        $foreign_found = true;
434 2
                        $skipping = true;
435 2
                        $offset = $other_offset;
436
                    }
437 2
                    $constraint_pos++;
438
                }
439
            } else {
440
                throw new NotSupportedException("Unexpected: $token");
441
            }
442
443 2
            if (!$skipping) {
444 2
                $ddl_fields_def .= $token . ' ';
445
            }
446
447
            /** Skip or keep until the next */
448 2
            while ($fields_definitions_tokens->offsetExists($offset)) {
449 2
                $skip_token = $fields_definitions_tokens[$offset];
450 2
                $skip_next = $fields_definitions_tokens[$offset + 1];
451
452 2
                if (!$skipping) {
453 2
                    $ddl_fields_def .= (string) $skip_token . ($skip_next == ',' ? '' : ' ');
454
                }
455
456 2
                $skipTokenType = $skip_token->getType();
0 ignored issues
show
Bug introduced by
The method getType() 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

456
                /** @scrutinizer ignore-call */ 
457
                $skipTokenType = $skip_token->getType();

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...
457
458 2
                if ($skipTokenType === SqlToken::TYPE_OPERATOR && $skip_token == ',') {
459 2
                    $ddl_fields_def .= "\n";
460 2
                    ++$offset;
461 2
                    $skipping = false;
462 2
                    break;
463
                }
464
465 2
                ++$offset;
466
            }
467
        }
468
469 2
        if (!$foreign_found) {
470
            throw new InvalidParamException("foreign key constraint '$name' not found in table '$table'");
471
        }
472
473 2
        $foreign_keys_state = $this->foreignKeysState();
474
475 2
        $return_queries[] = 'PRAGMA foreign_keys = 0';
476 2
        $return_queries[] = "SAVEPOINT drop_column_$unquoted_tablename";
477 2
        $return_queries[] = 'CREATE TABLE ' . $this->getDb()->quoteTableName("temp_$unquoted_tablename")
478 2
            . " AS SELECT * FROM $quoted_tablename";
479 2
        $return_queries[] = "DROP TABLE $quoted_tablename";
480 2
        $return_queries[] = "CREATE TABLE $quoted_tablename (" . trim($ddl_fields_def, " \n\r\t,") . ')';
481 2
        $return_queries[] = "INSERT INTO $quoted_tablename SELECT " . implode(',', $sql_fields_to_insert) . ' FROM '
482 2
             . $this->getDb()->quoteTableName("temp_$unquoted_tablename");
483 2
        $return_queries[] = 'DROP TABLE ' . $this->getDb()->quoteTableName("temp_$unquoted_tablename");
484 2
        $return_queries = array_merge($return_queries, $this->getIndexSqls($unquoted_tablename));
485 2
        $return_queries[] = "RELEASE drop_column_$unquoted_tablename";
486 2
        $return_queries[] = "PRAGMA foreign_keys = $foreign_keys_state";
487
488 2
        return implode(';', $return_queries);
489
    }
490
491
    /**
492
     * Builds a SQL statement for renaming a DB table.
493
     *
494
     * @param string $oldName the table to be renamed. The name will be properly quoted by the method.
495
     * @param string $newName the new table name. The name will be properly quoted by the method.
496
     *
497
     * @return string the SQL statement for renaming a DB table.
498
     */
499 3
    public function renameTable(string $oldName, string $newName): string
500
    {
501 3
        return 'ALTER TABLE ' . $this->getDb()->quoteTableName($oldName) . ' RENAME TO ' . $this->getDb()->quoteTableName($newName);
502
    }
503
504
    /**
505
     * Builds a SQL statement for changing the definition of a column.
506
     *
507
     * @param string $table the table whose column is to be changed. The table name will be properly quoted by the
508
     * method.
509
     * @param string $column the name of the column to be changed. The name will be properly quoted by the method.
510
     * @param string $type the new column type. The [[getColumnType()]] method will be invoked to convert abstract
511
     * column type (if any) into the physical one. Anything that is not recognized as abstract type will be kept in the
512
     * generated SQL. For example, 'string' will be turned into 'varchar(255)', while 'string not null' will become
513
     * 'varchar(255) not null'.
514
     *
515
     * @throws NotSupportedException this is not supported by SQLite.
516
     *
517
     * @return string the SQL statement for changing the definition of a column.
518
     */
519
    public function alterColumn(string $table, string $column, string $type): string
520
    {
521
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
522
    }
523
524
    /**
525
     * Builds a SQL statement for adding a primary key constraint to an existing table.
526
     *
527
     * @param string $name the name of the primary key constraint.
528
     * @param string $table the table that the primary key constraint will be added to.
529
     * @param array|string $columns comma separated string or array of columns that the primary key will consist of.
530
     *
531
     * @throws Exception|InvalidParamException
532
     *
533
     * @return string the SQL statement for adding a primary key constraint to an existing table.
534
     */
535
    public function addPrimaryKey(string $name, string $table, $columns): string
536
    {
537
        $return_queries = [];
538
        $schema = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $schema is dead and can be removed.
Loading history...
539
540
        if (($pos = strpos($table, '.')) !== false) {
541
            $schema = $this->unquoteTableName(substr($table, 0, $pos));
542
            $table = substr($table, $pos + 1);
543
            $unquoted_tablename = $schema . '.' . $this->unquoteTableName($table);
544
            $quoted_tablename = $schema . '.' . $this->getDb()->quoteTableName($table);
545
            $tmp_table_name = "temp_{$schema}_" . $this->unquoteTableName($table);
546
        } else {
547
            $unquoted_tablename = $this->unquoteTableName($table);
548
            $quoted_tablename = $this->getDb()->quoteTableName($table);
549
            $tmp_table_name = 'temp_' . $this->unquoteTableName($table);
550
        }
551
552
        $fields_definitions_tokens = $this->getFieldDefinitionsTokens($unquoted_tablename);
553
        $ddl_fields_defs = $fields_definitions_tokens->getSql();
554
        $ddl_fields_defs .= ', CONSTRAINT ' . $this->getDb()->quoteColumnName($name) . ' PRIMARY KEY (' .
555
            implode(',', (array)$columns) . ')';
556
        $foreign_keys_state = $this->foreignKeysState();
557
        $return_queries[] = 'PRAGMA foreign_keys = 0';
558
        $return_queries[] = "SAVEPOINT add_primary_key_to_$tmp_table_name";
559
        $return_queries[] = 'CREATE TABLE ' . $this->getDb()->quoteTableName($tmp_table_name) .
560
            " AS SELECT * FROM $quoted_tablename";
561
        $return_queries[] = "DROP TABLE $quoted_tablename";
562
        $return_queries[] = "CREATE TABLE $quoted_tablename (" . trim($ddl_fields_defs, " \n\r\t,") . ')';
563
        $return_queries[] = "INSERT INTO $quoted_tablename SELECT * FROM " . $this->getDb()->quoteTableName($tmp_table_name);
564
        $return_queries[] = 'DROP TABLE ' . $this->getDb()->quoteTableName($tmp_table_name);
565
566
        $return_queries = array_merge($return_queries, $this->getIndexSqls($unquoted_tablename));
567
568
        $return_queries[] = "RELEASE add_primary_key_to_$tmp_table_name";
569
        $return_queries[] = "PRAGMA foreign_keys = $foreign_keys_state";
570
571
        return implode(';', $return_queries);
572
    }
573
574
    /**
575
     * Builds a SQL statement for removing a primary key constraint to an existing table.
576
     *
577
     * @param string $name the name of the primary key constraint to be removed.
578
     * @param string $table the table that the primary key constraint will be removed from.
579
     *
580
     * @throws NotSupportedException this is not supported by SQLite.
581
     *
582
     * @return string the SQL statement for removing a primary key constraint from an existing table.
583
     */
584
    public function dropPrimaryKey(string $name, string $table): string
585
    {
586
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
587
    }
588
589
    /**
590
     * Creates a SQL command for adding an unique constraint to an existing table.
591
     *
592
     * @param string $name the name of the unique constraint. The name will be properly quoted by the method.
593
     * @param string $table the table that the unique constraint will be added to. The name will be properly quoted by
594
     * the method.
595
     * @param array|string $columns the name of the column to that the constraint will be added on. If there are
596
     * multiple columns, separate them with commas. The name will be properly quoted by the method.
597
     *
598
     * @throws Exception|InvalidArgumentException
599
     *
600
     * @return string the SQL statement for adding an unique constraint to an existing table.
601
     */
602 2
    public function addUnique(string $name, string $table, $columns): string
603
    {
604 2
        return $this->createIndex($name, $table, $columns, true);
605
    }
606
607
    /**
608
     * Creates a SQL command for dropping an unique constraint.
609
     *
610
     * @param string $name the name of the unique constraint to be dropped. The name will be properly quoted by the
611
     * method.
612
     * @param string $table the table whose unique constraint is to be dropped. The name will be properly quoted by the
613
     * method.
614
     *
615
     * @return string the SQL statement for dropping an unique constraint.
616
     */
617 2
    public function dropUnique(string $name, string $table): string
618
    {
619 2
        return "DROP INDEX $name";
620
    }
621
622
    /**
623
     * Creates a SQL command for adding a check constraint to an existing table.
624
     *
625
     * @param string $name the name of the check constraint. The name will be properly quoted by the method.
626
     * @param string $table the table that the check constraint will be added to. The name will be properly quoted by
627
     * the method.
628
     * @param string $expression the SQL of the `CHECK` constraint.
629
     *
630
     * @throws Exception|NotSupportedException
631
     *
632
     * @return string the SQL statement for adding a check constraint to an existing table.
633
     */
634
    public function addCheck(string $name, string $table, string $expression): string
635
    {
636
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
637
    }
638
639
    /**
640
     * Creates a SQL command for dropping a check constraint.
641
     *
642
     * @param string $name the name of the check constraint to be dropped. The name will be properly quoted by the
643
     * method.
644
     * @param string $table the table whose check constraint is to be dropped. The name will be properly quoted by the
645
     * method.
646
     *
647
     * @throws Exception|NotSupportedException
648
     *
649
     * @return string the SQL statement for dropping a check constraint.
650
     */
651
    public function dropCheck(string $name, string $table): string
652
    {
653
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
654
    }
655
656
    /**
657
     * Creates a SQL command for adding a default value constraint to an existing table.
658
     *
659
     * @param string $name the name of the default value constraint. The name will be properly quoted by the method.
660
     * @param string $table the table that the default value constraint will be added to. The name will be properly
661
     * quoted by the method.
662
     * @param string $column the name of the column to that the constraint will be added on. The name will be properly
663
     * quoted by the method.
664
     * @param mixed $value default value.
665
     *
666
     * @throws Exception|NotSupportedException if this is not supported by the underlying DBMS.
667
     *
668
     * @return string the SQL statement for adding a default value constraint to an existing table.
669
     */
670
    public function addDefaultValue(string $name, string $table, string $column, $value): string
671
    {
672
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
673
    }
674
675
    /**
676
     * Creates a SQL command for dropping a default value constraint.
677
     *
678
     * @param string $name the name of the default value constraint to be dropped. The name will be properly quoted by
679
     * the method.
680
     * @param string $table the table whose default value constraint is to be dropped. The name will be properly quoted
681
     * by the method.
682
     *
683
     * @throws Exception|NotSupportedException if this is not supported by the underlying DBMS.
684
     *
685
     * @return string the SQL statement for dropping a default value constraint.
686
     */
687
    public function dropDefaultValue(string $name, string $table): string
688
    {
689
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
690
    }
691
692
    /**
693
     * Builds a SQL command for adding comment to column.
694
     *
695
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
696
     * method.
697
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
698
     * method.
699
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
700
     *
701
     * @throws Exception|NotSupportedException if this is not supported by the underlying DBMS.
702
     *
703
     * @return string the SQL statement for adding comment on column.
704
     */
705
    public function addCommentOnColumn(string $table, string $column, string $comment): string
706
    {
707
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
708
    }
709
710
    /**
711
     * Builds a SQL command for adding comment to table.
712
     *
713
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
714
     * method.
715
     * @param string $comment the text of the comment to be added. The comment will be properly quoted by the method.
716
     *
717
     * @throws Exception|NotSupportedException if this is not supported by the underlying DBMS.
718
     *
719
     * @return string the SQL statement for adding comment on table.
720
     */
721
    public function addCommentOnTable(string $table, string $comment): string
722
    {
723
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
724
    }
725
726
    /**
727
     * Builds a SQL command for adding comment to column.
728
     *
729
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
730
     * method.
731
     * @param string $column the name of the column to be commented. The column name will be properly quoted by the
732
     * method.
733
     *
734
     * @throws Exception|NotSupportedException if this is not supported by the underlying DBMS.
735
     *
736
     * @return string the SQL statement for adding comment on column.
737
     */
738
    public function dropCommentFromColumn(string $table, string $column): string
739
    {
740
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
741
    }
742
743
    /**
744
     * Builds a SQL command for adding comment to table.
745
     *
746
     * @param string $table the table whose column is to be commented. The table name will be properly quoted by the
747
     * method.
748
     *
749
     * @throws Exception|NotSupportedException if this is not supported by the underlying DBMS.
750
     *
751
     * @return string the SQL statement for adding comment on column.
752
     */
753
    public function dropCommentFromTable(string $table): string
754
    {
755
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
756
    }
757
758
    /**
759
     * @param int|object|null $limit
760
     * @param int|object|null $offset
761
     *
762
     * @return string the LIMIT and OFFSET clauses.
763
     */
764 168
    public function buildLimit($limit, $offset): string
765
    {
766 168
        $sql = '';
767
768 168
        if ($this->hasLimit($limit)) {
769 9
            $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

769
            $sql = 'LIMIT ' . /** @scrutinizer ignore-type */ $limit;
Loading history...
770 9
            if ($this->hasOffset($offset)) {
771 9
                $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

771
                $sql .= ' OFFSET ' . /** @scrutinizer ignore-type */ $offset;
Loading history...
772
            }
773 162
        } elseif ($this->hasOffset($offset)) {
774
            /**
775
             * limit is not optional in SQLite.
776
             *
777
             * {@see http://www.sqlite.org/syntaxdiagrams.html#select-stmt}
778
             */
779
            $sql = "LIMIT 9223372036854775807 OFFSET $offset"; // 2^63-1
780
        }
781
782 168
        return $sql;
783
    }
784
785
    /**
786
     * Generates a SELECT SQL statement from a {@see Query} object.
787
     *
788
     * @param Query $query the {@see Query} object from which the SQL statement will be generated.
789
     * @param array $params the parameters to be bound to the generated SQL statement. These parameters will be included
790
     * in the result with the additional parameters generated during the query building process.
791
     *
792
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
793
     *
794
     * @return array the generated SQL statement (the first array element) and the corresponding parameters to be bound
795
     * to the SQL statement (the second array element). The parameters returned include those provided in `$params`.
796
     * @psalm-return array{string, array<array-key, mixed>}
797
     */
798 165
    public function build(Query $query, array $params = []): array
799
    {
800 165
        $query = $query->prepare($this);
801
802 165
        $params = empty($params) ? $query->getParams() : array_merge($params, $query->getParams());
803
804 165
        $clauses = [
805 165
            $this->buildSelect($query->getSelect(), $params, $query->getDistinct(), $query->getSelectOption()),
806 165
            $this->buildFrom($query->getFrom(), $params),
807 165
            $this->buildJoin($query->getJoin(), $params),
808 165
            $this->buildWhere($query->getWhere(), $params),
809 165
            $this->buildGroupBy($query->getGroupBy()),
810 165
            $this->buildHaving($query->getHaving(), $params),
811
        ];
812
813 165
        $sql = implode($this->separator, array_filter($clauses));
814 165
        $sql = $this->buildOrderByAndLimit($sql, $query->getOrderBy(), $query->getLimit(), $query->getOffset());
815
816 165
        if (!empty($query->getOrderBy())) {
817 6
            foreach ($query->getOrderBy() as $expression) {
818 6
                if ($expression instanceof ExpressionInterface) {
819 1
                    $this->buildExpression($expression, $params);
820
                }
821
            }
822
        }
823
824 165
        if (!empty($query->getGroupBy())) {
825 2
            foreach ($query->getGroupBy() as $expression) {
826 2
                if ($expression instanceof ExpressionInterface) {
827 1
                    $this->buildExpression($expression, $params);
828
                }
829
            }
830
        }
831
832 165
        $union = $this->buildUnion($query->getUnion(), $params);
833
834 165
        if ($union !== '') {
835 3
            $sql = "$sql{$this->separator}$union";
836
        }
837
838 165
        $with = $this->buildWithQueries($query->getWithQueries(), $params);
839
840 165
        if ($with !== '') {
841 2
            $sql = "$with{$this->separator}$sql";
842
        }
843
844 165
        return [$sql, $params];
845
    }
846
847
    /**
848
     * Builds a SQL statement for creating a new index.
849
     *
850
     * @param string $name the name of the index. The name will be properly quoted by the method.
851
     * @param string $table the table that the new index will be created for. The table name will be properly quoted by
852
     * the method.
853
     * @param array|string $columns the column(s) that should be included in the index. If there are multiple columns,
854
     * separate them with commas or use an array to represent them. Each column name will be properly quoted by the
855
     * method, unless a parenthesis is found in the name.
856
     * @param bool $unique whether to add UNIQUE constraint on the created index.
857
     *
858
     * @throws Exception|InvalidArgumentException
859
     *
860
     * @return string the SQL statement for creating a new index.
861
     */
862 8
    public function createIndex(string $name, string $table, $columns, bool $unique = false): string
863
    {
864 8
        $tableParts = explode('.', $table);
865
866 8
        $schema = null;
867 8
        if (count($tableParts) === 2) {
868 1
            [$schema, $table] = $tableParts;
869
        }
870
871 8
        return ($unique ? 'CREATE UNIQUE INDEX ' : 'CREATE INDEX ')
872 8
            . $this->getDb()->quoteTableName(($schema ? $schema . '.' : '') . $name) . ' ON '
873 8
            . $this->getDb()->quoteTableName($table)
874 8
            . ' (' . $this->buildColumns($columns) . ')';
875
    }
876
877
    /**
878
     * @param array $unions
879
     * @param array $params the binding parameters to be populated.
880
     *
881
     * @throws Exception|InvalidArgumentException|InvalidConfigException|NotSupportedException
882
     *
883
     * @return string the UNION clause built from {@see Query::$union}.
884
     */
885 165
    public function buildUnion(array $unions, array &$params = []): string
886
    {
887 165
        if (empty($unions)) {
888 165
            return '';
889
        }
890
891 3
        $result = '';
892
893 3
        foreach ($unions as $i => $union) {
894 3
            $query = $union['query'];
895 3
            if ($query instanceof Query) {
896 3
                [$unions[$i]['query'], $params] = $this->build($query, $params);
897
            }
898
899 3
            $result .= ' UNION ' . ($union['all'] ? 'ALL ' : '') . ' ' . $unions[$i]['query'];
900
        }
901
902 3
        return trim($result);
903
    }
904
905
    /**
906
     * Creates an SQL statement to insert rows into a database table if they do not already exist (matching unique
907
     * constraints), or update them if they do.
908
     *
909
     * For example,
910
     *
911
     * ```php
912
     * $sql = $queryBuilder->upsert('pages', [
913
     *     'name' => 'Front page',
914
     *     'url' => 'http://example.com/', // url is unique
915
     *     'visits' => 0,
916
     * ], [
917
     *     'visits' => new \Yiisoft\Db\Expression('visits + 1'),
918
     * ], $params);
919
     * ```
920
     *
921
     * The method will properly escape the table and column names.
922
     *
923
     * @param string $table the table that new rows will be inserted into/updated in.
924
     * @param array|Query $insertColumns the column data (name => value) to be inserted into the table or instance
925
     * of {@see Query} to perform `INSERT INTO ... SELECT` SQL statement.
926
     * @param array|bool $updateColumns the column data (name => value) to be updated if they already exist.
927
     * If `true` is passed, the column data will be updated to match the insert column data.
928
     * If `false` is passed, no update will be performed if the column data already exists.
929
     * @param array $params the binding parameters that will be generated by this method.
930
     * They should be bound to the DB command later.
931
     *
932
     * @throws Exception|InvalidConfigException|JsonException|NotSupportedException if this is not supported by the
933
     * underlying DBMS.
934
     *
935
     * @return string the resulting SQL.
936
     */
937 18
    public function upsert(string $table, $insertColumns, $updateColumns, array &$params): string
938
    {
939
        /** @var Constraint[] $constraints */
940 18
        $constraints = [];
941
942 18
        [$uniqueNames, $insertNames, $updateNames] = $this->prepareUpsertColumns(
943 18
            $table,
944
            $insertColumns,
945
            $updateColumns,
946
            $constraints
947
        );
948
949 18
        if (empty($uniqueNames)) {
950 3
            return $this->insert($table, $insertColumns, $params);
951
        }
952
953 15
        [, $placeholders, $values, $params] = $this->prepareInsertValues($table, $insertColumns, $params);
954
955 15
        $insertSql = 'INSERT OR IGNORE INTO ' . $this->getDb()->quoteTableName($table)
956 15
            . (!empty($insertNames) ? ' (' . implode(', ', $insertNames) . ')' : '')
957 15
            . (!empty($placeholders) ? ' VALUES (' . implode(', ', $placeholders) . ')' : $values);
958
959 15
        if ($updateColumns === false) {
960 5
            return $insertSql;
961
        }
962
963 10
        $updateCondition = ['or'];
964 10
        $quotedTableName = $this->getDb()->quoteTableName($table);
965
966 10
        foreach ($constraints as $constraint) {
967 10
            $constraintCondition = ['and'];
968 10
            foreach ($constraint->getColumnNames() as $name) {
969 10
                $quotedName = $this->getDb()->quoteColumnName($name);
970 10
                $constraintCondition[] = "$quotedTableName.$quotedName=(SELECT $quotedName FROM `EXCLUDED`)";
971
            }
972 10
            $updateCondition[] = $constraintCondition;
973
        }
974
975 10
        if ($updateColumns === true) {
976 4
            $updateColumns = [];
977 4
            foreach ($updateNames as $name) {
978 4
                $quotedName = $this->getDb()->quoteColumnName($name);
979
980 4
                if (strrpos($quotedName, '.') === false) {
981 4
                    $quotedName = "(SELECT $quotedName FROM `EXCLUDED`)";
982
                }
983 4
                $updateColumns[$name] = new Expression($quotedName);
984
            }
985
        }
986
987 10
        $updateSql = 'WITH "EXCLUDED" (' . implode(', ', $insertNames)
988 10
            . ') AS (' . (!empty($placeholders) ? 'VALUES (' . implode(', ', $placeholders) . ')'
989 10
            : ltrim($values, ' ')) . ') ' . $this->update($table, $updateColumns, $updateCondition, $params);
990
991 10
        return "$updateSql; $insertSql;";
992
    }
993
994 5
    private function unquoteTableName(string $tableName): string
995
    {
996 5
        return $this->getDb()->getSchema()->unquoteSimpleTableName($this->getDb()->quoteSql($tableName));
997
    }
998
999 5
    private function getFieldDefinitionsTokens(string $tableName): ?SqlToken
1000
    {
1001 5
        $create_table = $this->getCreateTable($tableName);
1002
1003
        /** Parse de CREATE TABLE statement to skip any use of this column, namely field definitions and FOREIGN KEYS */
1004 5
        $code = (new SqlTokenizer($create_table))->tokenize();
1005 5
        $pattern = (new SqlTokenizer('any CREATE any TABLE any()'))->tokenize();
1006 5
        if (!$code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
0 ignored issues
show
Bug introduced by
The method matches() 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

1006
        if (!$code[0]->/** @scrutinizer ignore-call */ matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {

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...
1007
            throw new InvalidParamException("Table not found: $tableName");
1008
        }
1009
1010
        /** Get the fields definition and foreign keys tokens */
1011 5
        return $code[0][$lastMatchIndex - 1];
1012
    }
1013
1014 5
    private function getCreateTable(string $tableName): string
1015
    {
1016 5
        if (($pos = strpos($tableName, '.')) !== false) {
1017
            $schema = substr($tableName, 0, $pos + 1);
1018
            $tableName = substr($tableName, $pos + 1);
1019
        } else {
1020 5
            $schema = '';
1021
        }
1022
1023 5
        $create_table = $this->getDb()->createCommand(
1024 5
            "select SQL from {$schema}SQLite_Master where tbl_name = '$tableName' and type='table'"
1025 5
        )->queryScalar();
1026
1027 5
        if ($create_table === null) {
1028
            throw new InvalidParamException("Table not found: $tableName");
1029
        }
1030
1031 5
        return trim($create_table);
0 ignored issues
show
Bug introduced by
It seems like $create_table can also be of type false; however, parameter $string of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1031
        return trim(/** @scrutinizer ignore-type */ $create_table);
Loading history...
1032
    }
1033
1034
    /**
1035
     * @return false|string|null
1036
     */
1037 5
    private function foreignKeysState()
1038
    {
1039 5
        return $this->getDb()->createCommand('PRAGMA foreign_keys')->queryScalar();
1040
    }
1041
1042 5
    private function getIndexSqls(string $tableName, $skipColumn = null, $newColumn = null): array
1043
    {
1044
        /** Get all indexes on this table */
1045 5
        $indexes = $this->getDb()->createCommand(
1046 5
            "select SQL from SQLite_Master where tbl_name = '$tableName' and type='index'"
1047 5
        )->queryAll();
1048
1049 5
        if ($skipColumn === null) {
1050 5
            return array_column($indexes, 'sql');
1051
        }
1052
1053
        $quoted_skip_column = $this->getDb()->quoteColumnName((string) $skipColumn);
1054
        if ($newColumn === null) {
1055
            /** Skip indexes which contain this column */
1056
            foreach ($indexes as $key => $index) {
1057
                $code = (new SqlTokenizer($index['sql']))->tokenize();
1058
                $pattern = (new SqlTokenizer('any CREATE any INDEX any ON any()'))->tokenize();
1059
1060
                /** Extract the list of fields of this index */
1061
                if (!$code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
1062
                    throw new InvalidParamException("Index definition error: $index");
1063
                }
1064
1065
                $found = false;
1066
                $indexFieldsDef = $code[0][$lastMatchIndex - 1];
1067
                $offset = 0;
1068
                while ($indexFieldsDef->offsetExists($offset)) {
0 ignored issues
show
Bug introduced by
The method offsetExists() 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

1068
                while ($indexFieldsDef->/** @scrutinizer ignore-call */ offsetExists($offset)) {

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...
1069
                    $token = $indexFieldsDef[$offset];
1070
                    $tokenType = $token->getType();
1071
                    if ($tokenType === SqlToken::TYPE_IDENTIFIER) {
1072
                        if ((string) $token === $skipColumn || (string) $token === $quoted_skip_column) {
1073
                            $found = true;
1074
                            unset($indexes[$key]);
1075
                            break;
1076
                        }
1077
                    }
1078
                    ++$offset;
1079
                }
1080
1081
                if (!$found) {
1082
                    /** If the index contains this column, do not add it */
1083
                    $indexes[$key] = $index['sql'];
1084
                }
1085
            }
1086
        } else {
1087
            foreach ($indexes as $key => $index) {
1088
                $code = (new SqlTokenizer($index['sql']))->tokenize();
1089
                $pattern = (new SqlTokenizer('any CREATE any INDEX any ON any ()'))->tokenize();
1090
1091
                /** Extract the list of fields of this index */
1092
                if (!$code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
1093
                    throw new InvalidParamException("Index definition error: $index");
1094
                }
1095
1096
                $indexFieldsDef = $code[0][$lastMatchIndex - 1];
1097
                $new_index_def = '';
1098
1099
                for ($i = 0; $i < $lastMatchIndex - 1; ++$i) {
1100
                    $new_index_def .= $code[0][$i] . ' ';
1101
                }
1102
1103
                $offset = 0;
1104
                while ($indexFieldsDef->offsetExists($offset)) {
1105
                    $token = $indexFieldsDef[$offset];
1106
                    $tokenType = $token->getType();
1107
                    if ($tokenType === SqlToken::TYPE_IDENTIFIER) {
1108
                        if ((string) $token === $skipColumn || (string) $token === $quoted_skip_column) {
1109
                            $token = $this->getDb()->quoteColumnName((string) $newColumn);
1110
                        }
1111
                    }
1112
                    $new_index_def .= $token;
1113
                    ++$offset;
1114
                }
1115
1116
                while ($code[0]->offsetExists($lastMatchIndex)) {
1117
                    $new_index_def .= $code[0][$lastMatchIndex++] . ' ';
1118
                }
1119
1120
                $indexes[$key] = $this->dropIndex((string) $code[0][2], $tableName) . ";$new_index_def";
1121
            }
1122
        }
1123
1124
        return $indexes;
1125
    }
1126
}
1127