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

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

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

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

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

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

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

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

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

1091
                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...
1092
                    $token = $indexFieldsDef[$offset];
1093
                    $tokenType = $token->getType();
1094
                    if ($tokenType === SqlToken::TYPE_IDENTIFIER) {
1095
                        if ((string) $token === $skipColumn || (string) $token === $quoted_skip_column) {
1096
                            $found = true;
1097
                            unset($indexes[$key]);
1098
                            break;
1099
                        }
1100
                    }
1101
                    ++$offset;
1102
                }
1103
1104
                if (!$found) {
1105
                    /** If the index contains this column, do not add it */
1106
                    $indexes[$key] = $index["sql"];
1107
                }
1108
            }
1109
        } else {
1110
            foreach ($indexes as $key => $index) {
1111
                $code = (new SqlTokenizer($index["sql"]))->tokenize();
1112
                $pattern = (new SqlTokenizer('any CREATE any INDEX any ON any ()'))->tokenize();
1113
1114
                /** Extract the list of fields of this index */
1115
                if (!$code[0]->matches($pattern, 0, $firstMatchIndex, $lastMatchIndex)) {
1116
                    throw new InvalidParamException("Index definition error: $index");
1117
                }
1118
1119
                $found = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $found is dead and can be removed.
Loading history...
1120
                $indexFieldsDef = $code[0][$lastMatchIndex - 1];
1121
                $new_index_def = '';
1122
1123
                for ($i = 0; $i < $lastMatchIndex - 1; ++$i) {
1124
                    $new_index_def .= (string)$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 .= (string)$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