Passed
Push — master ( edc9c1...558a8f )
by Alexander
08:28
created

QueryBuilder::addForeignKey()   B

Complexity

Conditions 9
Paths 36

Size

Total Lines 65
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 10.6845

Importance

Changes 0
Metric Value
cc 9
eloc 40
c 0
b 0
f 0
nc 36
nop 7
dl 0
loc 65
ccs 29
cts 40
cp 0.725
crap 10.6845
rs 7.7244

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

195
        /** @scrutinizer ignore-call */ 
196
        $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...
196
197 1
        if ($table !== null && $table->getSequenceName() !== null) {
198 1
            $tableName = $db->quoteTableName($tableName);
199 1
            if ($value === null) {
200 1
                $pk = $table->getPrimaryKey();
201 1
                $key = $this->db->quoteColumnName(\reset($pk));
202 1
                $value = $this->db->useMaster(static function (Connection $db) use ($key, $tableName) {
203 1
                    return $db->createCommand("SELECT MAX($key) FROM $tableName")->queryScalar();
204 1
                });
205
            } else {
206 1
                $value = (int) $value - 1;
207
            }
208
209 1
            return "UPDATE sqlite_sequence SET seq='$value' WHERE name='{$table->getName()}'";
210
        } elseif ($table === null) {
211
            throw new InvalidArgumentException("Table not found: $tableName");
212
        }
213
214
        throw new InvalidArgumentException("There is not sequence associated with table '$tableName'.'");
215
    }
216
217
    /**
218
     * Enables or disables integrity check.
219
     *
220
     * @param bool $check whether to turn on or off the integrity check.
221
     * @param string $schema the schema of the tables. Meaningless for SQLite.
222
     * @param string $table the table name. Meaningless for SQLite.
223
     *
224
     * @return string the SQL statement for checking integrity.
225
     */
226
    public function checkIntegrity(string $schema = '', string $table = '', bool $check = true): string
227
    {
228
        return 'PRAGMA foreign_keys=' . (int) $check;
229
    }
230
231
    /**
232
     * Builds a SQL statement for truncating a DB table.
233
     *
234
     * @param string $table the table to be truncated. The name will be properly quoted by the method.
235
     *
236
     * @throws Exception
237
     * @throws InvalidConfigException
238
     * @throws NotSupportedException
239
     *
240
     * @return string the SQL statement for truncating a DB table.
241
     */
242 1
    public function truncateTable(string $table): string
243
    {
244 1
        return 'DELETE FROM ' . $this->db->quoteTableName($table);
245
    }
246
247
    /**
248
     * Builds a SQL statement for dropping an index.
249
     *
250
     * @param string $name the name of the index to be dropped. The name will be properly quoted by the method.
251
     * @param string $table the table whose index is to be dropped. The name will be properly quoted by the method.
252
     *
253
     * @throws Exception
254
     * @throws InvalidConfigException
255
     * @throws NotSupportedException
256
     *
257
     * @return string the SQL statement for dropping an index.
258
     */
259 2
    public function dropIndex(string $name, string $table): string
260
    {
261 2
        return 'DROP INDEX ' . $this->db->quoteTableName($name);
262
    }
263
264
    /**
265
     * Builds a SQL statement for dropping a DB column.
266
     *
267
     * @param string $table the table whose column is to be dropped. The name will be properly quoted by the method.
268
     * @param string $column the name of the column to be dropped. The name will be properly quoted by the method.
269
     *
270
     * @throws NotSupportedException this is not supported by SQLite.
271
     *
272
     * @return string the SQL statement for dropping a DB column.
273
     */
274
    public function dropColumn(string $table, string $column): string
275
    {
276
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
277
    }
278
279
    /**
280
     * Builds a SQL statement for renaming a column.
281
     *
282
     * @param string $table the table whose column is to be renamed. The name will be properly quoted by the method.
283
     * @param string $oldName the old name of the column. The name will be properly quoted by the method.
284
     * @param string $newName the new name of the column. The name will be properly quoted by the method.
285
     *
286
     * @throws NotSupportedException this is not supported by SQLite.
287
     *
288
     * @return string the SQL statement for renaming a DB column.
289
     */
290
    public function renameColumn(string $table, string $oldName, string $newName): string
291
    {
292
        throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
293
    }
294
295
    /**
296
     * Builds a SQL statement for adding a foreign key constraint to an existing table.
297
     *
298
     * The method will properly quote the table and column names.
299
     *
300
     * @param string $name the name of the foreign key constraint.
301
     * @param string $table the table that the foreign key constraint will be added to.
302
     * @param string|array $columns the name of the column to that the constraint will be added on. If there are
303
     * multiple columns, separate them with commas or use an array to represent them.
304
     * @param string $refTable the table that the foreign key references to.
305
     * @param string|array $refColumns the name of the column that the foreign key references to. If there are multiple
306
     * columns, separate them with commas or use an array to represent them.
307
     * @param string $delete the ON DELETE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
308
     * SET DEFAULT, SET NULL.
309
     * @param string $update the ON UPDATE option. Most DBMS support these options: RESTRICT, CASCADE, NO ACTION,
310
     * SET DEFAULT, SET NULL.
311
     *
312
     * @throws Exception
313
     * @throws InvalidConfigException
314
     * @throws InvalidParamException
315
     * @throws NotSupportedException this is not supported by SQLite.
316
     *
317
     * @return string the SQL statement for adding a foreign key constraint to an existing table.
318
     */
319 2
    public function addForeignKey(
320
        string $name,
321
        string $table,
322
        $columns,
323
        string $refTable,
324
        $refColumns,
325
        ?string $delete = null,
326
        ?string $update = null
327
    ): string {
328 2
        $schema = $refschema = '';
329
330 2
        if (($pos = strpos($table, '.')) !== false) {
331
            $schema = $this->unquoteTableName(substr($table, 0, $pos));
332
            $table = substr($table, $pos + 1);
333
        }
334
335 2
        if (($pos_ref = strpos($refTable, '.')) !== false) {
336
            $refschema = substr($refTable, 0, $pos_ref);
337
            $refTable = substr($refTable, $pos_ref + 1);
338
        }
339
340 2
        if (($schema !== '' || ($refschema !== '' && $schema !== $refschema))) {
341
            return '' ;
342
        }
343
344 2
        if ($schema !== '') {
345
            $tmp_table_name =  "temp_{$schema}_" . $this->unquoteTableName($table);
346
            $schema .= '.';
347
            $unquoted_tablename = $schema . $this->unquoteTableName($table);
348
            $quoted_tablename = $schema . $this->db->quoteTableName($table);
349
        } else {
350 2
            $unquoted_tablename = $this->unquoteTableName($table);
351 2
            $quoted_tablename = $this->db->quoteTableName($table);
352 2
            $tmp_table_name =  "temp_" . $this->unquoteTableName($table);
353
        }
354
355 2
        $fields_definitions_tokens = $this->getFieldDefinitionsTokens($unquoted_tablename);
356 2
        $ddl_fields_defs = $fields_definitions_tokens->getSql();
357 2
        $ddl_fields_defs .= ",\nCONSTRAINT " . $this->db->quoteColumnName($name) . " FOREIGN KEY (" .
358 2
            implode(",", (array)$columns) . ") REFERENCES $refTable(" . implode(",", (array)$refColumns) . ")";
359
360 2
        if ($update !== null) {
361
            $ddl_fields_defs .= " ON UPDATE $update";
362
        }
363
364 2
        if ($delete !== null) {
365
            $ddl_fields_defs .= " ON DELETE $delete";
366
        }
367
368 2
        $foreign_keys_state = $this->foreignKeysState();
369 2
        $return_queries = [];
370 2
        $return_queries[] = "PRAGMA foreign_keys = off";
371 2
        $return_queries[] = "SAVEPOINT add_foreign_key_to_$tmp_table_name";
372 2
        $return_queries[] = "CREATE TEMP TABLE " . $this->db->quoteTableName($tmp_table_name)
373 2
            . " AS SELECT * FROM $quoted_tablename";
374 2
        $return_queries[] = "DROP TABLE $quoted_tablename";
375 2
        $return_queries[] = "CREATE TABLE $quoted_tablename (" . trim($ddl_fields_defs, " \n\r\t,") . ")";
376 2
        $return_queries[] = "INSERT INTO $quoted_tablename SELECT * FROM " . $this->db->quoteTableName($tmp_table_name);
377 2
        $return_queries[] = "DROP TABLE " . $this->db->quoteTableName($tmp_table_name);
378 2
        $return_queries = array_merge($return_queries, $this->getIndexSqls($unquoted_tablename));
379
380 2
        $return_queries[] = "RELEASE add_foreign_key_to_$tmp_table_name";
381 2
        $return_queries[] = "PRAGMA foreign_keys = $foreign_keys_state";
382
383 2
        return implode(";", $return_queries);
384
    }
385
386
    /**
387
     * Builds a SQL statement for dropping a foreign key constraint.
388
     *
389
     * @param string $name the name of the foreign key constraint to be dropped. The name will be properly quoted
390
     * by the method.
391
     * @param string $tableName
392
     *
393
     * @throws Exception
394
     * @throws InvalidConfigException
395
     * @throws InvalidParamException
396
     * @throws NotSupportedException
397
     *
398
     * @return string the SQL statement for dropping a foreign key constraint.
399
     */
400 1
    public function dropForeignKey(string $name, string $tableName): string
401
    {
402 1
        $return_queries = [];
403 1
        $ddl_fields_def = '';
404 1
        $sql_fields_to_insert = [];
405 1
        $skipping = false;
406 1
        $foreign_found = false;
407 1
        $quoted_foreign_name = $this->db->quoteColumnName($name);
408 1
        $quoted_tablename = $this->db->quoteTableName($tableName);
409 1
        $unquoted_tablename = $this->unquoteTableName($tableName);
410 1
        $fields_definitions_tokens = $this->getFieldDefinitionsTokens($unquoted_tablename);
411 1
        $offset = 0;
412 1
        $constraint_pos = 0;
413
414
        /** Traverse the tokens looking for either an identifier (field name) or a foreign key */
415 1
        while ($fields_definitions_tokens->offsetExists($offset)) {
416 1
            $token = $fields_definitions_tokens[$offset++];
417
            /**
418
             * These searchs could be done with another SqlTokenizer, but I don't konw how to do them, the documentation
419
             * for sqltokenizer si really scarse.
420
             */
421 1
            $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

421
            /** @scrutinizer ignore-call */ 
422
            $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...
422 1
            if ($tokenType === SqlToken::TYPE_IDENTIFIER) {
423 1
                $identifier = (string) $token;
424 1
                $sql_fields_to_insert[] = $identifier;
425 1
            } elseif ($tokenType === SqlToken::TYPE_KEYWORD) {
426 1
                $keyword = (string) $token;
427 1
                if ($keyword === 'CONSTRAINT' || $keyword === 'FOREIGN') {
428
                    /** Constraint key found */
429 1
                    $other_offset = $offset;
430 1
                    if ($keyword === 'CONSTRAINT') {
431 1
                        $constraint_name = (string) $fields_definitions_tokens[$other_offset];
432
                    } else {
433
                        $constraint_name = $this->db->quoteColumnName((string) $constraint_pos);
434
                    }
435 1
                    if (($constraint_name === $quoted_foreign_name) || (is_int($name) && $constraint_pos === $name)) {
436
                        /** Found foreign key $name, skip it */
437 1
                        $foreign_found = true;
438 1
                        $skipping = true;
439 1
                        $offset = $other_offset;
440
                    }
441 1
                    $constraint_pos++;
442
                }
443
            } else {
444
                throw new NotSupportedException("Unexpected: $token");
445
            }
446
447 1
            if (!$skipping) {
448 1
                $ddl_fields_def .= $token . " ";
449
            }
450
451
            /** Skip or keep until the next */
452 1
            while ($fields_definitions_tokens->offsetExists($offset)) {
453 1
                $skip_token = $fields_definitions_tokens[$offset];
454 1
                if (!$skipping) {
455 1
                    $ddl_fields_def .= (string)$skip_token . " ";
456
                }
457 1
                $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

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

787
            $sql = 'LIMIT ' . /** @scrutinizer ignore-type */ $limit;
Loading history...
788 8
            if ($this->hasOffset($offset)) {
789 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

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

1033
        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...
1034
            throw new InvalidParamException("Table not found: $tableName");
1035
        }
1036
1037
        /** Get the fields definition and foreign keys tokens */
1038 2
        return $code[0][$lastMatchIndex - 1];
1039
    }
1040
1041 2
    private function getCreateTable($tableName)
1042
    {
1043 2
        if (($pos = strpos($tableName, '.')) !== false) {
1044
            $schema = substr($tableName, 0, $pos + 1);
1045
            $tableName = substr($tableName, $pos + 1);
1046
        } else {
1047 2
            $schema = '';
1048
        }
1049
1050 2
        $create_table = $this->db->createCommand(
1051 2
            "select SQL from {$schema}SQLite_Master where tbl_name = '$tableName' and type='table'"
1052 2
        )->queryScalar();
1053
1054 2
        if ($create_table === null) {
1055
            throw new InvalidParamException("Table not found: $tableName");
1056
        }
1057
1058 2
        return trim($create_table);
0 ignored issues
show
Bug introduced by
It seems like $create_table can also be of type false; however, parameter $str 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

1058
        return trim(/** @scrutinizer ignore-type */ $create_table);
Loading history...
1059
    }
1060
1061 2
    private function foreignKeysState()
1062
    {
1063 2
        return $this->db->createCommand("PRAGMA foreign_keys")->queryScalar();
1064
    }
1065
1066 2
    private function getIndexSqls(string $tableName, $skipColumn = null, $newColumn = null): array
1067
    {
1068
        /** Get all indexes on this table */
1069 2
        $indexes = $this->db->createCommand(
1070 2
            "select SQL from SQLite_Master where tbl_name = '$tableName' and type='index'"
1071 2
        )->queryAll();
1072
1073 2
        if ($skipColumn === null) {
1074 2
            return array_column($indexes, "sql");
1075
        }
1076
1077
        $quoted_skip_column = $this->db->quoteColumnName((string) $skipColumn);
1078
        if ($newColumn === null) {
1079
            /** Skip indexes which contain this column */
1080
            foreach ($indexes as $key => $index) {
1081
                $code = (new SqlTokenizer($index["sql"]))->tokenize();
1082
                $pattern = (new SqlTokenizer('any CREATE any INDEX any ON any()'))->tokenize();
1083
1084
                /** Extract the list of fields of this index */
1085
                if (!$code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
1086
                    throw new InvalidParamException("Index definition error: $index");
1087
                }
1088
1089
                $found = false;
1090
                $indexFieldsDef = $code[0][$lastMatchIndex - 1];
1091
                $offset = 0;
1092
                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

1092
                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...
1093
                    $token = $indexFieldsDef[$offset];
1094
                    $tokenType = $token->getType();
1095
                    if ($tokenType === SqlToken::TYPE_IDENTIFIER) {
1096
                        if ((string) $token === $skipColumn || (string) $token === $quoted_skip_column) {
1097
                            $found = true;
1098
                            unset($indexes[$key]);
1099
                            break;
1100
                        }
1101
                    }
1102
                    ++$offset;
1103
                }
1104
1105
                if (!$found) {
1106
                    /** If the index contains this column, do not add it */
1107
                    $indexes[$key] = $index["sql"];
1108
                }
1109
            }
1110
        } else {
1111
            foreach ($indexes as $key => $index) {
1112
                $code = (new SqlTokenizer($index["sql"]))->tokenize();
1113
                $pattern = (new SqlTokenizer('any CREATE any INDEX any ON any ()'))->tokenize();
1114
1115
                /** Extract the list of fields of this index */
1116
                if (!$code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
1117
                    throw new InvalidParamException("Index definition error: $index");
1118
                }
1119
1120
                $indexFieldsDef = $code[0][$lastMatchIndex - 1];
1121
                $new_index_def = '';
1122
1123
                for ($i = 0; $i < $lastMatchIndex - 1; ++$i) {
1124
                    $new_index_def .= $code[0][$i] . " ";
1125
                }
1126
1127
                $offset = 0;
1128
                while ($indexFieldsDef->offsetExists($offset)) {
1129
                    $token = $indexFieldsDef[$offset];
1130
                    $tokenType = $token->getType();
1131
                    if ($tokenType === SqlToken::TYPE_IDENTIFIER) {
1132
                        if ((string) $token === $skipColumn || (string) $token === $quoted_skip_column) {
1133
                            $token = $this->db->quoteColumnName((string) $newColumn);
1134
                        }
1135
                    }
1136
                    $new_index_def .= $token;
1137
                    ++$offset;
1138
                }
1139
1140
                while ($code[0]->offsetExists($lastMatchIndex)) {
1141
                    $new_index_def .= $code[0][$lastMatchIndex++] . " ";
1142
                }
1143
1144
                $indexes[$key] = $this->dropIndex((string) $code[0][2], $tableName) . ";$new_index_def";
1145
            }
1146
        }
1147
1148
        return $indexes;
1149
    }
1150
}
1151